@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.js CHANGED
@@ -910,6 +910,14 @@ var KeyboardActions = class {
910
910
  }
911
911
  this.pasteCount = 0;
912
912
  }
913
+ cut() {
914
+ if (this.deps.isToolActive()) return;
915
+ this.copy();
916
+ this.deleteSelected();
917
+ }
918
+ hasClipboard() {
919
+ return this.clipboard.length > 0;
920
+ }
913
921
  paste() {
914
922
  if (this.deps.isToolActive()) return;
915
923
  this.flushPendingNudge();
@@ -978,6 +986,10 @@ var KeyboardActions = class {
978
986
  if (this.deps.isToolActive()) return;
979
987
  this.deps.ungroup?.();
980
988
  }
989
+ toggleLock() {
990
+ if (this.deps.isToolActive()) return;
991
+ this.deps.toggleLock?.();
992
+ }
981
993
  zOrder(operation) {
982
994
  if (this.deps.isToolActive()) return;
983
995
  this.flushPendingNudge();
@@ -1079,6 +1091,8 @@ var DEFAULT_BINDINGS = [
1079
1091
  ["zoom-reset", ["mod+0"]],
1080
1092
  ["group", ["mod+g"]],
1081
1093
  ["ungroup", ["mod+shift+g"]],
1094
+ ["cut", ["mod+x"]],
1095
+ ["toggle-lock", ["mod+shift+l"]],
1082
1096
  ["nudge-left", ["arrowleft"]],
1083
1097
  ["nudge-right", ["arrowright"]],
1084
1098
  ["nudge-up", ["arrowup"]],
@@ -1217,6 +1231,7 @@ var ShortcutMap = class {
1217
1231
  var ZOOM_SENSITIVITY = 1e-3;
1218
1232
  var ZOOM_STEP = 1.2;
1219
1233
  var MIDDLE_BUTTON = 1;
1234
+ var LONG_PRESS_MS = 500;
1220
1235
  var NUDGE_DELTAS = {
1221
1236
  "nudge-left": [-1, 0],
1222
1237
  "nudge-right": [1, 0],
@@ -1240,8 +1255,10 @@ var InputHandler = class {
1240
1255
  fitToContent: options.fitToContent,
1241
1256
  group: options.group,
1242
1257
  ungroup: options.ungroup,
1258
+ toggleLock: options.toggleLock,
1243
1259
  getLastPointerWorld: () => this.lastPointerWorld()
1244
1260
  });
1261
+ this.openContextMenu = options.openContextMenu;
1245
1262
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1246
1263
  this.scope = options.shortcuts?.scope ?? "focus";
1247
1264
  this.element.style.touchAction = "none";
@@ -1265,10 +1282,13 @@ var InputHandler = class {
1265
1282
  lastPointerEvent = null;
1266
1283
  inputFilter = new InputFilter();
1267
1284
  deferredDown = null;
1285
+ longPressTimer = null;
1286
+ longPressStart = null;
1268
1287
  abortController = new AbortController();
1269
1288
  actions;
1270
1289
  shortcutMap;
1271
1290
  scope;
1291
+ openContextMenu;
1272
1292
  setToolManager(toolManager, toolContext) {
1273
1293
  this.toolManager = toolManager;
1274
1294
  this.toolContext = toolContext;
@@ -1283,6 +1303,7 @@ var InputHandler = class {
1283
1303
  this.actions.dispose();
1284
1304
  this.abortController.abort();
1285
1305
  this.inputFilter.reset();
1306
+ this.cancelLongPress();
1286
1307
  this.deferredDown = null;
1287
1308
  this.lastPointerEvent = null;
1288
1309
  if (this.scope === "focus") {
@@ -1298,6 +1319,7 @@ var InputHandler = class {
1298
1319
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
1299
1320
  this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1300
1321
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1322
+ this.element.addEventListener("contextmenu", this.onContextMenu, opts);
1301
1323
  window.addEventListener("keydown", this.onKeyDown, opts);
1302
1324
  window.addEventListener("keyup", this.onKeyUp, opts);
1303
1325
  }
@@ -1326,11 +1348,13 @@ var InputHandler = class {
1326
1348
  this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1327
1349
  this.element.setPointerCapture?.(e.pointerId);
1328
1350
  if (this.activePointers.size === 2) {
1351
+ this.cancelLongPress();
1329
1352
  this.startPinch();
1330
1353
  this.cancelToolIfActive(e);
1331
1354
  return;
1332
1355
  }
1333
1356
  if (e.button === MIDDLE_BUTTON || e.button === 0 && this.spaceHeld) {
1357
+ this.cancelLongPress();
1334
1358
  this.isPanning = true;
1335
1359
  this.lastPointer = { x: e.clientX, y: e.clientY };
1336
1360
  return;
@@ -1340,6 +1364,7 @@ var InputHandler = class {
1340
1364
  if (result.action === "suppress") return;
1341
1365
  if (result.action === "defer") {
1342
1366
  this.deferredDown = e;
1367
+ this.startLongPress(e);
1343
1368
  return;
1344
1369
  }
1345
1370
  this.dispatchToolDown(e);
@@ -1368,6 +1393,7 @@ var InputHandler = class {
1368
1393
  } else if (this.deferredDown) {
1369
1394
  const result = this.inputFilter.filterMove(e);
1370
1395
  if (result.action === "dispatch") {
1396
+ this.cancelLongPress();
1371
1397
  this.dispatchToolDown(this.deferredDown);
1372
1398
  this.deferredDown = null;
1373
1399
  this.dispatchToolMove(e);
@@ -1377,6 +1403,7 @@ var InputHandler = class {
1377
1403
  }
1378
1404
  };
1379
1405
  onPointerUp = (e) => {
1406
+ this.cancelLongPress();
1380
1407
  try {
1381
1408
  this.element.releasePointerCapture(e.pointerId);
1382
1409
  } catch {
@@ -1429,74 +1456,82 @@ var InputHandler = class {
1429
1456
  runAction(action, e) {
1430
1457
  switch (action) {
1431
1458
  case "delete":
1432
- e.preventDefault();
1459
+ e?.preventDefault();
1433
1460
  this.actions.deleteSelected();
1434
1461
  return;
1435
1462
  case "deselect":
1436
1463
  this.actions.deselect();
1437
1464
  return;
1438
1465
  case "undo":
1439
- e.preventDefault();
1466
+ e?.preventDefault();
1440
1467
  this.actions.undo();
1441
1468
  return;
1442
1469
  case "redo":
1443
- e.preventDefault();
1470
+ e?.preventDefault();
1444
1471
  this.actions.redo();
1445
1472
  return;
1446
1473
  case "select-all":
1447
- e.preventDefault();
1474
+ e?.preventDefault();
1448
1475
  this.actions.selectAll();
1449
1476
  return;
1450
1477
  case "copy":
1451
- e.preventDefault();
1478
+ e?.preventDefault();
1452
1479
  this.actions.copy();
1453
1480
  return;
1454
1481
  case "paste":
1455
- e.preventDefault();
1482
+ e?.preventDefault();
1456
1483
  this.actions.paste();
1457
1484
  return;
1458
1485
  case "duplicate":
1459
- e.preventDefault();
1486
+ e?.preventDefault();
1460
1487
  this.actions.duplicate();
1461
1488
  return;
1462
1489
  case "z-forward":
1463
- e.preventDefault();
1490
+ e?.preventDefault();
1464
1491
  this.actions.zOrder("forward");
1465
1492
  return;
1466
1493
  case "z-backward":
1467
- e.preventDefault();
1494
+ e?.preventDefault();
1468
1495
  this.actions.zOrder("backward");
1469
1496
  return;
1470
1497
  case "z-front":
1471
- e.preventDefault();
1498
+ e?.preventDefault();
1472
1499
  this.actions.zOrder("front");
1473
1500
  return;
1474
1501
  case "z-back":
1475
- e.preventDefault();
1502
+ e?.preventDefault();
1476
1503
  this.actions.zOrder("back");
1477
1504
  return;
1478
1505
  case "zoom-fit":
1479
- e.preventDefault();
1506
+ e?.preventDefault();
1480
1507
  this.actions.zoomToFit();
1481
1508
  return;
1482
1509
  case "group":
1483
- e.preventDefault();
1510
+ e?.preventDefault();
1484
1511
  this.actions.group();
1485
1512
  return;
1486
1513
  case "ungroup":
1487
- e.preventDefault();
1514
+ e?.preventDefault();
1488
1515
  this.actions.ungroup();
1489
1516
  return;
1517
+ case "cut":
1518
+ e?.preventDefault();
1519
+ this.actions.cut();
1520
+ return;
1521
+ case "toggle-lock":
1522
+ e?.preventDefault();
1523
+ this.actions.toggleLock();
1524
+ return;
1490
1525
  case "zoom-in":
1491
- e.preventDefault();
1526
+ e?.preventDefault();
1492
1527
  this.zoomByFactor(ZOOM_STEP);
1493
1528
  return;
1494
1529
  case "zoom-out":
1495
- e.preventDefault();
1530
+ e?.preventDefault();
1496
1531
  this.zoomByFactor(1 / ZOOM_STEP);
1497
1532
  return;
1498
1533
  case "zoom-reset":
1499
- e.preventDefault();
1534
+ e?.preventDefault();
1500
1535
  this.zoomToLevel(1);
1501
1536
  return;
1502
1537
  case "nudge-left":
@@ -1504,22 +1539,26 @@ var InputHandler = class {
1504
1539
  case "nudge-up":
1505
1540
  case "nudge-down": {
1506
1541
  const delta = NUDGE_DELTAS[action];
1507
- if (delta && this.actions.nudge(delta[0], delta[1], e.shiftKey)) {
1508
- e.preventDefault();
1542
+ if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
1543
+ e?.preventDefault();
1509
1544
  }
1510
1545
  return;
1511
1546
  }
1512
1547
  default:
1513
1548
  if (action.startsWith("tool:")) {
1514
1549
  if (this.isToolActive) return;
1515
- e.preventDefault();
1550
+ e?.preventDefault();
1516
1551
  this.toolContext?.switchTool?.(action.slice("tool:".length));
1517
1552
  return;
1518
1553
  }
1519
1554
  console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1520
1555
  }
1521
1556
  }
1557
+ hasClipboard() {
1558
+ return this.actions.hasClipboard();
1559
+ }
1522
1560
  startPinch() {
1561
+ this.cancelLongPress();
1523
1562
  this.inputFilter.reset();
1524
1563
  this.deferredDown = null;
1525
1564
  this.isPanning = true;
@@ -1562,6 +1601,13 @@ var InputHandler = class {
1562
1601
  const rect = this.element.getBoundingClientRect();
1563
1602
  return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1564
1603
  }
1604
+ onContextMenu = (e) => {
1605
+ e.preventDefault();
1606
+ if (this.toolManager?.activeTool?.name !== "select") return;
1607
+ const rect = this.element.getBoundingClientRect();
1608
+ const world = this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1609
+ this.openContextMenu?.({ x: e.clientX, y: e.clientY }, world);
1610
+ };
1565
1611
  onPointerLeave = (e) => {
1566
1612
  this.lastPointerEvent = null;
1567
1613
  this.onPointerUp(e);
@@ -1609,12 +1655,36 @@ var InputHandler = class {
1609
1655
  this.element.focus({ preventScroll: true });
1610
1656
  }
1611
1657
  cancelToolIfActive(e) {
1658
+ this.cancelLongPress();
1612
1659
  if (this.isToolActive) {
1613
1660
  this.dispatchToolUp(e);
1614
1661
  this.isToolActive = false;
1615
1662
  }
1616
1663
  this.deferredDown = null;
1617
1664
  }
1665
+ startLongPress(e) {
1666
+ if (e.pointerType !== "touch") return;
1667
+ if (this.toolManager?.activeTool?.name !== "select") return;
1668
+ this.longPressStart = { x: e.clientX, y: e.clientY };
1669
+ this.longPressTimer = setTimeout(() => this.fireLongPress(), LONG_PRESS_MS);
1670
+ }
1671
+ cancelLongPress() {
1672
+ if (this.longPressTimer !== null) {
1673
+ clearTimeout(this.longPressTimer);
1674
+ this.longPressTimer = null;
1675
+ }
1676
+ this.longPressStart = null;
1677
+ }
1678
+ fireLongPress() {
1679
+ this.longPressTimer = null;
1680
+ if (!this.deferredDown || this.activePointers.size !== 1 || this.isPanning) return;
1681
+ const start = this.longPressStart;
1682
+ if (!start) return;
1683
+ const rect = this.element.getBoundingClientRect();
1684
+ const world = this.camera.screenToWorld({ x: start.x - rect.left, y: start.y - rect.top });
1685
+ this.deferredDown = null;
1686
+ this.openContextMenu?.({ x: start.x, y: start.y }, world);
1687
+ }
1618
1688
  };
1619
1689
 
1620
1690
  // src/canvas/background.ts
@@ -1932,9 +2002,10 @@ var Quadtree = class {
1932
2002
  };
1933
2003
 
1934
2004
  // src/elements/stroke-smoothing.ts
1935
- var MIN_PRESSURE_SCALE = 0.2;
2005
+ var MIN_PRESSURE_SCALE = 0.4;
2006
+ var MAX_PRESSURE_SCALE = 1.8;
1936
2007
  function pressureToWidth(pressure, baseWidth) {
1937
- return baseWidth * (MIN_PRESSURE_SCALE + (1 - MIN_PRESSURE_SCALE) * pressure);
2008
+ return baseWidth * (MIN_PRESSURE_SCALE + (MAX_PRESSURE_SCALE - MIN_PRESSURE_SCALE) * pressure);
1938
2009
  }
1939
2010
  function simplifyPoints(points, tolerance) {
1940
2011
  if (points.length <= 2) return points.slice();
@@ -3597,12 +3668,33 @@ var NoteToolbar = class {
3597
3668
  e.stopPropagation();
3598
3669
  });
3599
3670
  select.addEventListener("change", () => {
3600
- setFontSize(Number(select.value));
3671
+ this.applyFontSize(Number(select.value));
3601
3672
  this.updateActiveStates();
3602
3673
  this.anchor?.focus();
3603
3674
  });
3604
3675
  return select;
3605
3676
  }
3677
+ applyFontSize(size) {
3678
+ const sel = window.getSelection();
3679
+ const collapsed = !sel || sel.rangeCount === 0 || sel.getRangeAt(0).collapsed;
3680
+ if (collapsed && this.anchor) {
3681
+ const range = document.createRange();
3682
+ range.selectNodeContents(this.anchor);
3683
+ sel?.removeAllRanges();
3684
+ sel?.addRange(range);
3685
+ setFontSize(size);
3686
+ const after = window.getSelection();
3687
+ if (after) {
3688
+ const caret = document.createRange();
3689
+ caret.selectNodeContents(this.anchor);
3690
+ caret.collapse(false);
3691
+ after.removeAllRanges();
3692
+ after.addRange(caret);
3693
+ }
3694
+ return;
3695
+ }
3696
+ setFontSize(size);
3697
+ }
3606
3698
  positionToolbar(anchor) {
3607
3699
  if (!this.el) return;
3608
3700
  const rect = anchor.getBoundingClientRect();
@@ -3801,6 +3893,87 @@ var NoteEditor = class {
3801
3893
  }
3802
3894
  };
3803
3895
 
3896
+ // src/canvas/context-menu.ts
3897
+ var ContextMenu = class {
3898
+ constructor(options) {
3899
+ this.options = options;
3900
+ }
3901
+ el = null;
3902
+ outsideListener = null;
3903
+ keyListener = null;
3904
+ isOpen() {
3905
+ return this.el !== null;
3906
+ }
3907
+ open(items, screenPos) {
3908
+ this.close();
3909
+ const el = document.createElement("div");
3910
+ el.className = "fieldnotes-context-menu";
3911
+ Object.assign(el.style, {
3912
+ position: "fixed",
3913
+ left: `${screenPos.x}px`,
3914
+ top: `${screenPos.y}px`,
3915
+ zIndex: "10000",
3916
+ display: "flex",
3917
+ flexDirection: "column"
3918
+ });
3919
+ for (const item of items) {
3920
+ const btn = document.createElement("button");
3921
+ btn.type = "button";
3922
+ btn.className = "fieldnotes-context-menu-item" + (item.disabled ? " fieldnotes-context-menu-item--disabled" : "");
3923
+ btn.textContent = item.label;
3924
+ if (item.disabled) {
3925
+ btn.disabled = true;
3926
+ } else {
3927
+ btn.addEventListener("click", () => {
3928
+ this.options.onCommand(item.action);
3929
+ this.close();
3930
+ });
3931
+ }
3932
+ el.appendChild(btn);
3933
+ }
3934
+ document.body.appendChild(el);
3935
+ this.el = el;
3936
+ this.clampToViewport(el, screenPos);
3937
+ this.keyListener = (e) => {
3938
+ if (e.key === "Escape") this.close();
3939
+ };
3940
+ document.addEventListener("keydown", this.keyListener);
3941
+ this.outsideListener = (e) => {
3942
+ if (this.el && !this.el.contains(e.target)) this.close();
3943
+ };
3944
+ setTimeout(() => {
3945
+ if (this.outsideListener) document.addEventListener("pointerdown", this.outsideListener);
3946
+ }, 0);
3947
+ }
3948
+ close() {
3949
+ if (this.keyListener) {
3950
+ document.removeEventListener("keydown", this.keyListener);
3951
+ this.keyListener = null;
3952
+ }
3953
+ if (this.outsideListener) {
3954
+ document.removeEventListener("pointerdown", this.outsideListener);
3955
+ this.outsideListener = null;
3956
+ }
3957
+ if (this.el) {
3958
+ this.el.remove();
3959
+ this.el = null;
3960
+ this.options.onClose();
3961
+ }
3962
+ }
3963
+ dispose() {
3964
+ this.close();
3965
+ }
3966
+ clampToViewport(el, screenPos) {
3967
+ const rect = el.getBoundingClientRect();
3968
+ if (rect.width > 0 && screenPos.x + rect.width > window.innerWidth) {
3969
+ el.style.left = `${Math.max(0, screenPos.x - rect.width)}px`;
3970
+ }
3971
+ if (rect.height > 0 && screenPos.y + rect.height > window.innerHeight) {
3972
+ el.style.top = `${Math.max(0, screenPos.y - rect.height)}px`;
3973
+ }
3974
+ }
3975
+ };
3976
+
3804
3977
  // src/elements/translate.ts
3805
3978
  function translateElementPatch(el, dx, dy) {
3806
3979
  const position = { x: el.position.x + dx, y: el.position.y + dy };
@@ -5617,8 +5790,20 @@ var Viewport = class {
5617
5790
  fitToContent: () => this.fitToContent(),
5618
5791
  group: () => this.groupSelection(),
5619
5792
  ungroup: () => this.ungroupSelection(),
5793
+ toggleLock: () => this.toggleLockSelection(),
5794
+ openContextMenu: (screenPos, world) => {
5795
+ this.getSelectTool()?.selectAtPoint(world, this.toolContext);
5796
+ this.openContextMenu(screenPos);
5797
+ },
5620
5798
  shortcuts: options.shortcuts
5621
5799
  });
5800
+ if (options.contextMenu !== false) {
5801
+ this.contextMenu = new ContextMenu({
5802
+ onCommand: (action) => this.runAction(action),
5803
+ onClose: noop
5804
+ });
5805
+ }
5806
+ this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
5622
5807
  this.domNodeManager = new DomNodeManager({
5623
5808
  domLayer: this.domLayer,
5624
5809
  onEditRequest: (id) => this.startEditingElement(id),
@@ -5650,6 +5835,7 @@ var Viewport = class {
5650
5835
  this.unsubCamera = this.camera.onChange(() => {
5651
5836
  this.applyCameraTransform();
5652
5837
  this.noteEditor.updateToolbarPosition();
5838
+ this.contextMenu?.close();
5653
5839
  this.requestRender();
5654
5840
  });
5655
5841
  this.unsubStore = [
@@ -5702,6 +5888,7 @@ var Viewport = class {
5702
5888
  canvasEl;
5703
5889
  wrapper;
5704
5890
  unsubCamera;
5891
+ unsubToolChange;
5705
5892
  unsubStore;
5706
5893
  inputHandler;
5707
5894
  background;
@@ -5724,6 +5911,7 @@ var Viewport = class {
5724
5911
  doubleTapDetector = new DoubleTapDetector();
5725
5912
  tapDownX = 0;
5726
5913
  tapDownY = 0;
5914
+ contextMenu = null;
5727
5915
  get ctx() {
5728
5916
  return this.canvasEl.getContext("2d");
5729
5917
  }
@@ -5914,6 +6102,34 @@ var Viewport = class {
5914
6102
  getSelectedIds() {
5915
6103
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5916
6104
  }
6105
+ runAction(action) {
6106
+ this.inputHandler.runAction(action);
6107
+ }
6108
+ canPaste() {
6109
+ return this.inputHandler.hasClipboard();
6110
+ }
6111
+ openContextMenu(screenPos) {
6112
+ if (!this.contextMenu) return;
6113
+ const ids = this.getSelectedIds();
6114
+ const items = [];
6115
+ if (ids.length > 0) {
6116
+ items.push({ label: "Cut", action: "cut" });
6117
+ items.push({ label: "Copy", action: "copy" });
6118
+ if (this.canPaste()) items.push({ label: "Paste", action: "paste" });
6119
+ items.push({ label: "Duplicate", action: "duplicate" });
6120
+ items.push({ label: "Delete", action: "delete" });
6121
+ items.push({ label: "Bring to Front", action: "z-front" });
6122
+ items.push({ label: "Bring Forward", action: "z-forward" });
6123
+ items.push({ label: "Send Backward", action: "z-backward" });
6124
+ items.push({ label: "Send to Back", action: "z-back" });
6125
+ const allLocked = ids.every((id) => this.store.getById(id)?.locked);
6126
+ items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
6127
+ } else if (this.canPaste()) {
6128
+ items.push({ label: "Paste", action: "paste" });
6129
+ }
6130
+ if (items.length === 0) return;
6131
+ this.contextMenu.open(items, screenPos);
6132
+ }
5917
6133
  onSelectionChange(listener) {
5918
6134
  const tool = this.getSelectTool();
5919
6135
  return tool ? tool.onSelectionChange(listener) : noop;
@@ -5974,6 +6190,20 @@ var Viewport = class {
5974
6190
  }
5975
6191
  this.historyRecorder.commit();
5976
6192
  }
6193
+ toggleLockSelection() {
6194
+ const ids = this.getSelectedIds();
6195
+ if (ids.length === 0) return;
6196
+ const anyUnlocked = ids.some((id) => {
6197
+ const el = this.store.getById(id);
6198
+ return el ? !el.locked : false;
6199
+ });
6200
+ this.historyRecorder.begin();
6201
+ for (const id of ids) {
6202
+ const el = this.store.getById(id);
6203
+ if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6204
+ }
6205
+ this.historyRecorder.commit();
6206
+ }
5977
6207
  alignSelection(edge) {
5978
6208
  const bounded = this.boundedSelection();
5979
6209
  if (bounded.length < 2) return;
@@ -6072,12 +6302,14 @@ var Viewport = class {
6072
6302
  this.noteEditor.destroy(this.store);
6073
6303
  this.arrowLabelEditor.cancel();
6074
6304
  this.historyRecorder.destroy();
6305
+ this.contextMenu?.dispose();
6075
6306
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
6076
6307
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
6077
6308
  this.wrapper.removeEventListener("dragover", this.onDragOver);
6078
6309
  this.wrapper.removeEventListener("drop", this.onDrop);
6079
6310
  this.inputHandler.destroy();
6080
6311
  this.unsubCamera();
6312
+ this.unsubToolChange();
6081
6313
  this.unsubStore.forEach((fn) => fn());
6082
6314
  this.resizeObserver?.disconnect();
6083
6315
  this.resizeObserver = null;
@@ -6905,6 +7137,15 @@ var SelectTool = class {
6905
7137
  this.setSelectedIds(ids);
6906
7138
  this.ctx?.requestRender();
6907
7139
  }
7140
+ selectAtPoint(world, ctx) {
7141
+ const hit = this.hitTest(world, ctx);
7142
+ if (!hit) {
7143
+ this.setSelectedIds([]);
7144
+ return;
7145
+ }
7146
+ if (this._selectedIds.includes(hit.id)) return;
7147
+ this.setSelectedIds(expandToGroups([hit.id], ctx.store.getAll()));
7148
+ }
6908
7149
  get isMarqueeActive() {
6909
7150
  return this.mode.type === "marquee";
6910
7151
  }
@@ -7408,6 +7649,7 @@ var SelectTool = class {
7408
7649
  for (const id of this._selectedIds) {
7409
7650
  const el = ctx.store.getById(id);
7410
7651
  if (!el || !("size" in el)) continue;
7652
+ if (el.locked) continue;
7411
7653
  if (el.type === "shape" && el.shape === "line") continue;
7412
7654
  const layout = this.getOverlayLayout(el, zoom);
7413
7655
  if (!layout) continue;
@@ -7543,62 +7785,89 @@ var SelectTool = class {
7543
7785
  canvasCtx.stroke();
7544
7786
  }
7545
7787
  }
7546
- if ("size" in el) {
7547
- canvasCtx.setLineDash([]);
7548
- canvasCtx.fillStyle = "#ffffff";
7549
- const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7550
- for (const [, pos] of corners) {
7788
+ if (!el.locked) {
7789
+ if ("size" in el) {
7790
+ canvasCtx.setLineDash([]);
7791
+ canvasCtx.fillStyle = "#ffffff";
7792
+ const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7793
+ for (const [, pos] of corners) {
7794
+ canvasCtx.fillRect(
7795
+ pos.x - handleWorldSize / 2,
7796
+ pos.y - handleWorldSize / 2,
7797
+ handleWorldSize,
7798
+ handleWorldSize
7799
+ );
7800
+ canvasCtx.strokeRect(
7801
+ pos.x - handleWorldSize / 2,
7802
+ pos.y - handleWorldSize / 2,
7803
+ handleWorldSize,
7804
+ handleWorldSize
7805
+ );
7806
+ }
7807
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7808
+ } else if (el.type === "template") {
7809
+ canvasCtx.setLineDash([]);
7810
+ canvasCtx.fillStyle = "#ffffff";
7811
+ const hx = bounds.x + bounds.w;
7812
+ const hy = bounds.y + bounds.h;
7551
7813
  canvasCtx.fillRect(
7552
- pos.x - handleWorldSize / 2,
7553
- pos.y - handleWorldSize / 2,
7814
+ hx - handleWorldSize / 2,
7815
+ hy - handleWorldSize / 2,
7554
7816
  handleWorldSize,
7555
7817
  handleWorldSize
7556
7818
  );
7557
7819
  canvasCtx.strokeRect(
7558
- pos.x - handleWorldSize / 2,
7559
- pos.y - handleWorldSize / 2,
7820
+ hx - handleWorldSize / 2,
7821
+ hy - handleWorldSize / 2,
7560
7822
  handleWorldSize,
7561
7823
  handleWorldSize
7562
7824
  );
7825
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7826
+ }
7827
+ if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7828
+ const stemStart = this.topMidpoint(layout);
7829
+ const stemEnd = layout.rotateHandle;
7830
+ canvasCtx.beginPath();
7831
+ canvasCtx.moveTo(stemStart.x, stemStart.y);
7832
+ canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7833
+ canvasCtx.stroke();
7834
+ canvasCtx.setLineDash([]);
7835
+ canvasCtx.fillStyle = "#ffffff";
7836
+ canvasCtx.beginPath();
7837
+ canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7838
+ canvasCtx.fill();
7839
+ canvasCtx.stroke();
7840
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7563
7841
  }
7564
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7565
- } else if (el.type === "template") {
7566
- canvasCtx.setLineDash([]);
7567
- canvasCtx.fillStyle = "#ffffff";
7568
- const hx = bounds.x + bounds.w;
7569
- const hy = bounds.y + bounds.h;
7570
- canvasCtx.fillRect(
7571
- hx - handleWorldSize / 2,
7572
- hy - handleWorldSize / 2,
7573
- handleWorldSize,
7574
- handleWorldSize
7575
- );
7576
- canvasCtx.strokeRect(
7577
- hx - handleWorldSize / 2,
7578
- hy - handleWorldSize / 2,
7579
- handleWorldSize,
7580
- handleWorldSize
7581
- );
7582
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7583
7842
  }
7584
- if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7585
- const stemStart = this.topMidpoint(layout);
7586
- const stemEnd = layout.rotateHandle;
7587
- canvasCtx.beginPath();
7588
- canvasCtx.moveTo(stemStart.x, stemStart.y);
7589
- canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7590
- canvasCtx.stroke();
7591
- canvasCtx.setLineDash([]);
7592
- canvasCtx.fillStyle = "#ffffff";
7593
- canvasCtx.beginPath();
7594
- canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7595
- canvasCtx.fill();
7596
- canvasCtx.stroke();
7597
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7843
+ if (el.locked) {
7844
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7845
+ if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7598
7846
  }
7599
7847
  }
7600
7848
  canvasCtx.restore();
7601
7849
  }
7850
+ drawLockBadge(ctx, at, zoom) {
7851
+ const r = 9 / zoom;
7852
+ ctx.save();
7853
+ ctx.setLineDash([]);
7854
+ ctx.beginPath();
7855
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7856
+ ctx.fillStyle = "#ffffff";
7857
+ ctx.fill();
7858
+ ctx.strokeStyle = "#2196F3";
7859
+ ctx.lineWidth = 1.5 / zoom;
7860
+ ctx.stroke();
7861
+ const bw = 8 / zoom;
7862
+ const bh = 6 / zoom;
7863
+ ctx.fillStyle = "#2196F3";
7864
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7865
+ ctx.beginPath();
7866
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7867
+ ctx.lineWidth = 1.4 / zoom;
7868
+ ctx.stroke();
7869
+ ctx.restore();
7870
+ }
7602
7871
  renderBindingHighlights(canvasCtx, arrow, zoom) {
7603
7872
  if (!this.ctx) return;
7604
7873
  if (!arrow.fromBinding && !arrow.toBinding) return;
@@ -8571,7 +8840,7 @@ var TemplateTool = class {
8571
8840
  };
8572
8841
 
8573
8842
  // src/index.ts
8574
- var VERSION = "0.35.0";
8843
+ var VERSION = "0.37.0";
8575
8844
  export {
8576
8845
  ArrowTool,
8577
8846
  AutoSave,