@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/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
@@ -3801,6 +3871,87 @@ var NoteEditor = class {
3801
3871
  }
3802
3872
  };
3803
3873
 
3874
+ // src/canvas/context-menu.ts
3875
+ var ContextMenu = class {
3876
+ constructor(options) {
3877
+ this.options = options;
3878
+ }
3879
+ el = null;
3880
+ outsideListener = null;
3881
+ keyListener = null;
3882
+ isOpen() {
3883
+ return this.el !== null;
3884
+ }
3885
+ open(items, screenPos) {
3886
+ this.close();
3887
+ const el = document.createElement("div");
3888
+ el.className = "fieldnotes-context-menu";
3889
+ Object.assign(el.style, {
3890
+ position: "fixed",
3891
+ left: `${screenPos.x}px`,
3892
+ top: `${screenPos.y}px`,
3893
+ zIndex: "10000",
3894
+ display: "flex",
3895
+ flexDirection: "column"
3896
+ });
3897
+ for (const item of items) {
3898
+ const btn = document.createElement("button");
3899
+ btn.type = "button";
3900
+ btn.className = "fieldnotes-context-menu-item" + (item.disabled ? " fieldnotes-context-menu-item--disabled" : "");
3901
+ btn.textContent = item.label;
3902
+ if (item.disabled) {
3903
+ btn.disabled = true;
3904
+ } else {
3905
+ btn.addEventListener("click", () => {
3906
+ this.options.onCommand(item.action);
3907
+ this.close();
3908
+ });
3909
+ }
3910
+ el.appendChild(btn);
3911
+ }
3912
+ document.body.appendChild(el);
3913
+ this.el = el;
3914
+ this.clampToViewport(el, screenPos);
3915
+ this.keyListener = (e) => {
3916
+ if (e.key === "Escape") this.close();
3917
+ };
3918
+ document.addEventListener("keydown", this.keyListener);
3919
+ this.outsideListener = (e) => {
3920
+ if (this.el && !this.el.contains(e.target)) this.close();
3921
+ };
3922
+ setTimeout(() => {
3923
+ if (this.outsideListener) document.addEventListener("pointerdown", this.outsideListener);
3924
+ }, 0);
3925
+ }
3926
+ close() {
3927
+ if (this.keyListener) {
3928
+ document.removeEventListener("keydown", this.keyListener);
3929
+ this.keyListener = null;
3930
+ }
3931
+ if (this.outsideListener) {
3932
+ document.removeEventListener("pointerdown", this.outsideListener);
3933
+ this.outsideListener = null;
3934
+ }
3935
+ if (this.el) {
3936
+ this.el.remove();
3937
+ this.el = null;
3938
+ this.options.onClose();
3939
+ }
3940
+ }
3941
+ dispose() {
3942
+ this.close();
3943
+ }
3944
+ clampToViewport(el, screenPos) {
3945
+ const rect = el.getBoundingClientRect();
3946
+ if (rect.width > 0 && screenPos.x + rect.width > window.innerWidth) {
3947
+ el.style.left = `${Math.max(0, screenPos.x - rect.width)}px`;
3948
+ }
3949
+ if (rect.height > 0 && screenPos.y + rect.height > window.innerHeight) {
3950
+ el.style.top = `${Math.max(0, screenPos.y - rect.height)}px`;
3951
+ }
3952
+ }
3953
+ };
3954
+
3804
3955
  // src/elements/translate.ts
3805
3956
  function translateElementPatch(el, dx, dy) {
3806
3957
  const position = { x: el.position.x + dx, y: el.position.y + dy };
@@ -5617,8 +5768,20 @@ var Viewport = class {
5617
5768
  fitToContent: () => this.fitToContent(),
5618
5769
  group: () => this.groupSelection(),
5619
5770
  ungroup: () => this.ungroupSelection(),
5771
+ toggleLock: () => this.toggleLockSelection(),
5772
+ openContextMenu: (screenPos, world) => {
5773
+ this.getSelectTool()?.selectAtPoint(world, this.toolContext);
5774
+ this.openContextMenu(screenPos);
5775
+ },
5620
5776
  shortcuts: options.shortcuts
5621
5777
  });
5778
+ if (options.contextMenu !== false) {
5779
+ this.contextMenu = new ContextMenu({
5780
+ onCommand: (action) => this.runAction(action),
5781
+ onClose: noop
5782
+ });
5783
+ }
5784
+ this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
5622
5785
  this.domNodeManager = new DomNodeManager({
5623
5786
  domLayer: this.domLayer,
5624
5787
  onEditRequest: (id) => this.startEditingElement(id),
@@ -5650,6 +5813,7 @@ var Viewport = class {
5650
5813
  this.unsubCamera = this.camera.onChange(() => {
5651
5814
  this.applyCameraTransform();
5652
5815
  this.noteEditor.updateToolbarPosition();
5816
+ this.contextMenu?.close();
5653
5817
  this.requestRender();
5654
5818
  });
5655
5819
  this.unsubStore = [
@@ -5702,6 +5866,7 @@ var Viewport = class {
5702
5866
  canvasEl;
5703
5867
  wrapper;
5704
5868
  unsubCamera;
5869
+ unsubToolChange;
5705
5870
  unsubStore;
5706
5871
  inputHandler;
5707
5872
  background;
@@ -5724,6 +5889,7 @@ var Viewport = class {
5724
5889
  doubleTapDetector = new DoubleTapDetector();
5725
5890
  tapDownX = 0;
5726
5891
  tapDownY = 0;
5892
+ contextMenu = null;
5727
5893
  get ctx() {
5728
5894
  return this.canvasEl.getContext("2d");
5729
5895
  }
@@ -5914,6 +6080,34 @@ var Viewport = class {
5914
6080
  getSelectedIds() {
5915
6081
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
5916
6082
  }
6083
+ runAction(action) {
6084
+ this.inputHandler.runAction(action);
6085
+ }
6086
+ canPaste() {
6087
+ return this.inputHandler.hasClipboard();
6088
+ }
6089
+ openContextMenu(screenPos) {
6090
+ if (!this.contextMenu) return;
6091
+ const ids = this.getSelectedIds();
6092
+ const items = [];
6093
+ if (ids.length > 0) {
6094
+ items.push({ label: "Cut", action: "cut" });
6095
+ items.push({ label: "Copy", action: "copy" });
6096
+ if (this.canPaste()) items.push({ label: "Paste", action: "paste" });
6097
+ items.push({ label: "Duplicate", action: "duplicate" });
6098
+ items.push({ label: "Delete", action: "delete" });
6099
+ items.push({ label: "Bring to Front", action: "z-front" });
6100
+ items.push({ label: "Bring Forward", action: "z-forward" });
6101
+ items.push({ label: "Send Backward", action: "z-backward" });
6102
+ items.push({ label: "Send to Back", action: "z-back" });
6103
+ const allLocked = ids.every((id) => this.store.getById(id)?.locked);
6104
+ items.push({ label: allLocked ? "Unlock" : "Lock", action: "toggle-lock" });
6105
+ } else if (this.canPaste()) {
6106
+ items.push({ label: "Paste", action: "paste" });
6107
+ }
6108
+ if (items.length === 0) return;
6109
+ this.contextMenu.open(items, screenPos);
6110
+ }
5917
6111
  onSelectionChange(listener) {
5918
6112
  const tool = this.getSelectTool();
5919
6113
  return tool ? tool.onSelectionChange(listener) : noop;
@@ -5974,6 +6168,20 @@ var Viewport = class {
5974
6168
  }
5975
6169
  this.historyRecorder.commit();
5976
6170
  }
6171
+ toggleLockSelection() {
6172
+ const ids = this.getSelectedIds();
6173
+ if (ids.length === 0) return;
6174
+ const anyUnlocked = ids.some((id) => {
6175
+ const el = this.store.getById(id);
6176
+ return el ? !el.locked : false;
6177
+ });
6178
+ this.historyRecorder.begin();
6179
+ for (const id of ids) {
6180
+ const el = this.store.getById(id);
6181
+ if (el && el.locked !== anyUnlocked) this.store.update(id, { locked: anyUnlocked });
6182
+ }
6183
+ this.historyRecorder.commit();
6184
+ }
5977
6185
  alignSelection(edge) {
5978
6186
  const bounded = this.boundedSelection();
5979
6187
  if (bounded.length < 2) return;
@@ -6072,12 +6280,14 @@ var Viewport = class {
6072
6280
  this.noteEditor.destroy(this.store);
6073
6281
  this.arrowLabelEditor.cancel();
6074
6282
  this.historyRecorder.destroy();
6283
+ this.contextMenu?.dispose();
6075
6284
  this.wrapper.removeEventListener("pointerdown", this.onTapDown);
6076
6285
  this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
6077
6286
  this.wrapper.removeEventListener("dragover", this.onDragOver);
6078
6287
  this.wrapper.removeEventListener("drop", this.onDrop);
6079
6288
  this.inputHandler.destroy();
6080
6289
  this.unsubCamera();
6290
+ this.unsubToolChange();
6081
6291
  this.unsubStore.forEach((fn) => fn());
6082
6292
  this.resizeObserver?.disconnect();
6083
6293
  this.resizeObserver = null;
@@ -6905,6 +7115,15 @@ var SelectTool = class {
6905
7115
  this.setSelectedIds(ids);
6906
7116
  this.ctx?.requestRender();
6907
7117
  }
7118
+ selectAtPoint(world, ctx) {
7119
+ const hit = this.hitTest(world, ctx);
7120
+ if (!hit) {
7121
+ this.setSelectedIds([]);
7122
+ return;
7123
+ }
7124
+ if (this._selectedIds.includes(hit.id)) return;
7125
+ this.setSelectedIds(expandToGroups([hit.id], ctx.store.getAll()));
7126
+ }
6908
7127
  get isMarqueeActive() {
6909
7128
  return this.mode.type === "marquee";
6910
7129
  }
@@ -7408,6 +7627,7 @@ var SelectTool = class {
7408
7627
  for (const id of this._selectedIds) {
7409
7628
  const el = ctx.store.getById(id);
7410
7629
  if (!el || !("size" in el)) continue;
7630
+ if (el.locked) continue;
7411
7631
  if (el.type === "shape" && el.shape === "line") continue;
7412
7632
  const layout = this.getOverlayLayout(el, zoom);
7413
7633
  if (!layout) continue;
@@ -7543,62 +7763,89 @@ var SelectTool = class {
7543
7763
  canvasCtx.stroke();
7544
7764
  }
7545
7765
  }
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) {
7766
+ if (!el.locked) {
7767
+ if ("size" in el) {
7768
+ canvasCtx.setLineDash([]);
7769
+ canvasCtx.fillStyle = "#ffffff";
7770
+ const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7771
+ for (const [, pos] of corners) {
7772
+ canvasCtx.fillRect(
7773
+ pos.x - handleWorldSize / 2,
7774
+ pos.y - handleWorldSize / 2,
7775
+ handleWorldSize,
7776
+ handleWorldSize
7777
+ );
7778
+ canvasCtx.strokeRect(
7779
+ pos.x - handleWorldSize / 2,
7780
+ pos.y - handleWorldSize / 2,
7781
+ handleWorldSize,
7782
+ handleWorldSize
7783
+ );
7784
+ }
7785
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7786
+ } else if (el.type === "template") {
7787
+ canvasCtx.setLineDash([]);
7788
+ canvasCtx.fillStyle = "#ffffff";
7789
+ const hx = bounds.x + bounds.w;
7790
+ const hy = bounds.y + bounds.h;
7551
7791
  canvasCtx.fillRect(
7552
- pos.x - handleWorldSize / 2,
7553
- pos.y - handleWorldSize / 2,
7792
+ hx - handleWorldSize / 2,
7793
+ hy - handleWorldSize / 2,
7554
7794
  handleWorldSize,
7555
7795
  handleWorldSize
7556
7796
  );
7557
7797
  canvasCtx.strokeRect(
7558
- pos.x - handleWorldSize / 2,
7559
- pos.y - handleWorldSize / 2,
7798
+ hx - handleWorldSize / 2,
7799
+ hy - handleWorldSize / 2,
7560
7800
  handleWorldSize,
7561
7801
  handleWorldSize
7562
7802
  );
7803
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7804
+ }
7805
+ if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7806
+ const stemStart = this.topMidpoint(layout);
7807
+ const stemEnd = layout.rotateHandle;
7808
+ canvasCtx.beginPath();
7809
+ canvasCtx.moveTo(stemStart.x, stemStart.y);
7810
+ canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7811
+ canvasCtx.stroke();
7812
+ canvasCtx.setLineDash([]);
7813
+ canvasCtx.fillStyle = "#ffffff";
7814
+ canvasCtx.beginPath();
7815
+ canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7816
+ canvasCtx.fill();
7817
+ canvasCtx.stroke();
7818
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7563
7819
  }
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
7820
  }
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]);
7821
+ if (el.locked) {
7822
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7823
+ if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7598
7824
  }
7599
7825
  }
7600
7826
  canvasCtx.restore();
7601
7827
  }
7828
+ drawLockBadge(ctx, at, zoom) {
7829
+ const r = 9 / zoom;
7830
+ ctx.save();
7831
+ ctx.setLineDash([]);
7832
+ ctx.beginPath();
7833
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7834
+ ctx.fillStyle = "#ffffff";
7835
+ ctx.fill();
7836
+ ctx.strokeStyle = "#2196F3";
7837
+ ctx.lineWidth = 1.5 / zoom;
7838
+ ctx.stroke();
7839
+ const bw = 8 / zoom;
7840
+ const bh = 6 / zoom;
7841
+ ctx.fillStyle = "#2196F3";
7842
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7843
+ ctx.beginPath();
7844
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7845
+ ctx.lineWidth = 1.4 / zoom;
7846
+ ctx.stroke();
7847
+ ctx.restore();
7848
+ }
7602
7849
  renderBindingHighlights(canvasCtx, arrow, zoom) {
7603
7850
  if (!this.ctx) return;
7604
7851
  if (!arrow.fromBinding && !arrow.toBinding) return;
@@ -8571,7 +8818,7 @@ var TemplateTool = class {
8571
8818
  };
8572
8819
 
8573
8820
  // src/index.ts
8574
- var VERSION = "0.35.0";
8821
+ var VERSION = "0.36.0";
8575
8822
  export {
8576
8823
  ArrowTool,
8577
8824
  AutoSave,