@tldraw/editor 5.5.0-next.f9308d324e1c → 5.5.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.
Files changed (192) hide show
  1. package/dist-cjs/index.d.ts +49 -23
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +16 -12
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +42 -3
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +5 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +2 -5
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  12. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
  13. package/dist-cjs/lib/editor/Editor.js +168 -181
  14. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
  16. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -2
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
  20. package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +49 -63
  22. package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +24 -24
  26. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +7 -1
  28. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +4 -3
  30. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
  34. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
  35. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  36. package/dist-cjs/lib/exports/ExportDelay.js +10 -2
  37. package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
  38. package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
  39. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  40. package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
  41. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  42. package/dist-cjs/lib/exports/fetchCache.js +3 -0
  43. package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
  44. package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
  45. package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
  46. package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
  47. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  48. package/dist-cjs/lib/exports/parseCss.js +1 -1
  49. package/dist-cjs/lib/exports/parseCss.js.map +1 -1
  50. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
  52. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useIdentity.js +6 -2
  54. package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
  55. package/dist-cjs/lib/license/LicenseManager.js +9 -1
  56. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  57. package/dist-cjs/lib/license/LicenseProvider.js +12 -4
  58. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  59. package/dist-cjs/lib/utils/debug-flags.js +0 -6
  60. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  61. package/dist-cjs/lib/utils/deepLinks.js +2 -1
  62. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  63. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +32 -25
  64. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
  65. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +81 -40
  66. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  67. package/dist-cjs/version.js +3 -3
  68. package/dist-cjs/version.js.map +1 -1
  69. package/dist-esm/index.d.mts +49 -23
  70. package/dist-esm/index.mjs +4 -2
  71. package/dist-esm/index.mjs.map +2 -2
  72. package/dist-esm/lib/TldrawEditor.mjs +17 -13
  73. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  74. package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
  75. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  76. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
  77. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  78. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
  79. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  80. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  81. package/dist-esm/lib/editor/Editor.mjs +168 -181
  82. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  83. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
  84. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  85. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -2
  86. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  87. package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
  88. package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
  89. package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +49 -63
  90. package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +1 -1
  92. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  93. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +24 -24
  94. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  95. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +7 -1
  96. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  97. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +4 -3
  98. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  99. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
  100. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  101. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
  102. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
  103. package/dist-esm/lib/exports/ExportDelay.mjs +11 -3
  104. package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
  105. package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
  106. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  107. package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
  108. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  109. package/dist-esm/lib/exports/fetchCache.mjs +3 -0
  110. package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
  111. package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
  112. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
  113. package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
  114. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  115. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  116. package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
  117. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  118. package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
  119. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  120. package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
  121. package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
  122. package/dist-esm/lib/license/LicenseManager.mjs +9 -1
  123. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  124. package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
  125. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  126. package/dist-esm/lib/utils/debug-flags.mjs +0 -6
  127. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  128. package/dist-esm/lib/utils/deepLinks.mjs +2 -1
  129. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  130. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +32 -25
  131. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
  132. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +81 -40
  133. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  134. package/dist-esm/version.mjs +3 -3
  135. package/dist-esm/version.mjs.map +1 -1
  136. package/editor.css +9 -1
  137. package/package.json +8 -11
  138. package/src/index.ts +5 -1
  139. package/src/lib/TldrawEditor.tsx +35 -21
  140. package/src/lib/components/MenuClickCapture.tsx +55 -4
  141. package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
  142. package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
  143. package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
  144. package/src/lib/editor/Editor.test.ts +65 -0
  145. package/src/lib/editor/Editor.ts +261 -294
  146. package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
  147. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +1 -0
  148. package/src/lib/editor/managers/ClickManager/ClickManager.ts +3 -2
  149. package/src/lib/editor/managers/EditorManager.test.ts +32 -1
  150. package/src/lib/editor/managers/EditorManager.ts +15 -3
  151. package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +80 -0
  152. package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +54 -81
  153. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +1 -1
  154. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +32 -36
  155. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +13 -2
  156. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +13 -0
  157. package/src/lib/editor/managers/SnapManager/SnapManager.ts +6 -4
  158. package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
  159. package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
  160. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
  161. package/src/lib/editor/types/external-content.ts +5 -0
  162. package/src/lib/exports/ExportDelay.tsx +16 -4
  163. package/src/lib/exports/FontEmbedder.test.ts +99 -0
  164. package/src/lib/exports/FontEmbedder.ts +32 -11
  165. package/src/lib/exports/StyleEmbedder.ts +13 -4
  166. package/src/lib/exports/fetchCache.test.ts +22 -0
  167. package/src/lib/exports/fetchCache.ts +4 -0
  168. package/src/lib/exports/getSvgAsImage.ts +13 -5
  169. package/src/lib/exports/getSvgJsx.test.ts +43 -1
  170. package/src/lib/exports/getSvgJsx.tsx +28 -16
  171. package/src/lib/exports/parseCss.test.ts +4 -0
  172. package/src/lib/exports/parseCss.ts +2 -2
  173. package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
  174. package/src/lib/hooks/useCanvasEvents.ts +42 -0
  175. package/src/lib/hooks/useIdentity.tsx +12 -1
  176. package/src/lib/license/LicenseManager.ts +13 -1
  177. package/src/lib/license/LicenseProvider.test.tsx +76 -0
  178. package/src/lib/license/LicenseProvider.tsx +19 -5
  179. package/src/lib/test/preserveFocus.test.tsx +81 -0
  180. package/src/lib/utils/debug-flags.ts +0 -6
  181. package/src/lib/utils/deepLinks.test.ts +15 -0
  182. package/src/lib/utils/deepLinks.ts +8 -3
  183. package/src/lib/utils/sync/LocalIndexedDb.test.ts +121 -1
  184. package/src/lib/utils/sync/LocalIndexedDb.ts +41 -28
  185. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +257 -1
  186. package/src/lib/utils/sync/TLLocalSyncClient.ts +112 -54
  187. package/src/version.ts +3 -3
  188. package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
  189. package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
  190. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
  191. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
  192. package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
@@ -136,6 +136,10 @@ import { UserPreferencesManager } from "./managers/UserPreferencesManager/UserPr
136
136
  import { OverlayManager } from "./overlays/OverlayManager.mjs";
137
137
  import { RootState } from "./tools/RootState.mjs";
138
138
  const RENDERING_SHAPES_SORT_CACHE_THRESHOLD = 100;
139
+ const AXIS = {
140
+ horizontal: { val: "x", min: "minX", max: "maxX", dim: "width" },
141
+ vertical: { val: "y", min: "minY", max: "maxY", dim: "height" }
142
+ };
139
143
  class Editor extends EventEmitter {
140
144
  id = uniqueId();
141
145
  constructor({
@@ -462,7 +466,7 @@ class Editor extends EventEmitter {
462
466
  }
463
467
  }
464
468
  if (deleteBindingIds.length) {
465
- this.deleteBindings(deleteBindingIds);
469
+ this.store.remove(deleteBindingIds);
466
470
  }
467
471
  }
468
472
  },
@@ -1422,6 +1426,7 @@ class Editor extends EventEmitter {
1422
1426
  * @public
1423
1427
  **/
1424
1428
  updateDocumentSettings(settings) {
1429
+ if (this.getIsReadonly()) return this;
1425
1430
  this.run(
1426
1431
  () => {
1427
1432
  this.store.put([{ ...this.getDocumentSettings(), ...settings }]);
@@ -1444,7 +1449,7 @@ class Editor extends EventEmitter {
1444
1449
  updateInstanceState(partial, historyOptions) {
1445
1450
  this._updateInstanceState(partial, { history: "ignore", ...historyOptions });
1446
1451
  if (partial.isChangingStyle !== void 0) {
1447
- clearTimeout(this._isChangingStyleTimeout);
1452
+ this.timers.clearTimeout(this._isChangingStyleTimeout);
1448
1453
  if (partial.isChangingStyle === true) {
1449
1454
  this._isChangingStyleTimeout = this.timers.setTimeout(() => {
1450
1455
  this._updateInstanceState({ isChangingStyle: false }, { history: "ignore" });
@@ -1581,7 +1586,7 @@ class Editor extends EventEmitter {
1581
1586
  * @public
1582
1587
  */
1583
1588
  select(...shapes) {
1584
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
1589
+ const ids = toShapeIds(shapes);
1585
1590
  this.setSelectedShapes(ids);
1586
1591
  return this;
1587
1592
  }
@@ -1596,7 +1601,7 @@ class Editor extends EventEmitter {
1596
1601
  * @public
1597
1602
  */
1598
1603
  deselect(...shapes) {
1599
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
1604
+ const ids = toShapeIds(shapes);
1600
1605
  const selectedShapeIds = this.getSelectedShapeIds();
1601
1606
  if (selectedShapeIds.length > 0 && ids.length > 0) {
1602
1607
  this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id)));
@@ -1634,7 +1639,7 @@ class Editor extends EventEmitter {
1634
1639
  }
1635
1640
  const ids = this.getSortedChildIdsForParent(parentToSelectWithinId);
1636
1641
  if (ids.length <= 0) return this;
1637
- this.setSelectedShapes(this._getUnlockedShapeIds(ids));
1642
+ this.setSelectedShapes(ids.filter((id) => !this.getShape(id)?.isLocked));
1638
1643
  return this;
1639
1644
  }
1640
1645
  /**
@@ -1651,12 +1656,15 @@ class Editor extends EventEmitter {
1651
1656
  const selectedShapeIds = this.getSelectedShapeIds();
1652
1657
  const firstParentId = selectedShapeIds[0] ? this.getShape(selectedShapeIds[0])?.parentId : null;
1653
1658
  const isSelectedWithinContainer = firstParentId && selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) && !isPageId(firstParentId);
1654
- const filteredShapes = isSelectedWithinContainer ? this.getCurrentPageShapes().filter((shape2) => shape2.parentId === firstParentId) : this.getCurrentPageShapes().filter((shape2) => isPageId(shape2.parentId));
1655
- const readingOrderShapes = isSelectedWithinContainer ? this._getShapesInReadingOrder(filteredShapes) : this.getCurrentPageShapesInReadingOrder();
1659
+ const filteredShapes = this.getCurrentPageShapes().filter(
1660
+ (shape2) => !this.isShapeOrAncestorLocked(shape2) && (isSelectedWithinContainer ? shape2.parentId === firstParentId : isPageId(shape2.parentId))
1661
+ );
1662
+ const readingOrderShapes = this._getShapesInReadingOrder(filteredShapes);
1656
1663
  const currentShapeId = selectedShapeIds.length === 1 ? selectedShapeIds[0] : readingOrderShapes.find((shape2) => selectedShapeIds.includes(shape2.id))?.id;
1657
1664
  let adjacentShapeId;
1658
1665
  if (direction === "next" || direction === "prev") {
1659
1666
  const shapeIds = readingOrderShapes.map((shape2) => shape2.id);
1667
+ if (shapeIds.length === 0) return;
1660
1668
  const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1;
1661
1669
  const startIndex = currentIndex === -1 && direction === "prev" ? 0 : currentIndex;
1662
1670
  const adjacentIndex = (startIndex + (direction === "next" ? 1 : -1) + shapeIds.length) % shapeIds.length;
@@ -1758,13 +1766,14 @@ class Editor extends EventEmitter {
1758
1766
  });
1759
1767
  if (shapesInDirection.length === 0) return currentShapeId;
1760
1768
  const lowestScoringShape = minBy(shapesInDirection, ({ center }) => {
1761
- const distance = Vec.Dist2(currentCenter, center);
1769
+ const distance = Vec.Dist(currentCenter, center);
1762
1770
  const dirProp = ["left", "right"].includes(direction) ? "x" : "y";
1763
1771
  const directionalDistance = Math.abs(center[dirProp] - currentCenter[dirProp]);
1764
1772
  const offProp = ["left", "right"].includes(direction) ? "y" : "x";
1765
1773
  const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp]);
1766
- const angle = Math.abs(Vec.Angle(currentCenter, center) * (180 / Math.PI));
1767
- const angleDeviation = Math.abs(angle - directionToAngle[direction]);
1774
+ const angle = (Vec.Angle(currentCenter, center) * (180 / Math.PI) + 360) % 360;
1775
+ const rawAngleDeviation = Math.abs(angle - directionToAngle[direction]);
1776
+ const angleDeviation = Math.min(rawAngleDeviation, 360 - rawAngleDeviation);
1768
1777
  return distance * 1 + // Base distance
1769
1778
  offAxisDeviation * 2 + // Heavy penalty for off-axis deviation
1770
1779
  (distance - directionalDistance) * 1.5 + // Penalty for diagonal distance
@@ -2102,7 +2111,7 @@ class Editor extends EventEmitter {
2102
2111
  * @public
2103
2112
  */
2104
2113
  setHintingShapes(shapes) {
2105
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
2114
+ const ids = toShapeIds(shapes);
2106
2115
  this.run(
2107
2116
  () => {
2108
2117
  this._updateCurrentPageState({ hintingShapeIds: dedupe(ids) });
@@ -2132,7 +2141,7 @@ class Editor extends EventEmitter {
2132
2141
  * @public
2133
2142
  */
2134
2143
  setErasingShapes(shapes) {
2135
- const ids = typeof shapes[0] === "string" ? shapes.slice() : shapes.map((shape) => shape.id);
2144
+ const ids = toShapeIds(shapes).slice();
2136
2145
  ids.sort();
2137
2146
  const erasingShapeIds = this.getErasingShapeIds();
2138
2147
  this.run(
@@ -2539,14 +2548,16 @@ class Editor extends EventEmitter {
2539
2548
  setCamera(point, opts) {
2540
2549
  const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture();
2541
2550
  if (isLocked && !opts?.force) return this;
2551
+ const _point = new Vec(point.x, point.y, point.z ?? this.getZoomLevel());
2552
+ if (!Number.isFinite(_point.x) || !Number.isFinite(_point.y) || !Number.isFinite(_point.z)) {
2553
+ throw Error(
2554
+ `Editor.setCamera: expected finite values, got (${_point.x}, ${_point.y}, ${_point.z}).`
2555
+ );
2556
+ }
2542
2557
  this.stopCameraAnimation();
2543
2558
  if (this.getInstanceState().followingUserId) {
2544
2559
  this.stopFollowingUser();
2545
2560
  }
2546
- const _point = Vec.Cast(point);
2547
- if (!Number.isFinite(_point.x)) _point.x = 0;
2548
- if (!Number.isFinite(_point.y)) _point.y = 0;
2549
- if (_point.z === void 0 || !Number.isFinite(_point.z)) point.z = this.getZoomLevel();
2550
2561
  const camera = this.getConstrainedCamera(_point, opts);
2551
2562
  if (opts?.animation) {
2552
2563
  const { width, height } = this.getViewportScreenBounds();
@@ -2831,11 +2842,14 @@ class Editor extends EventEmitter {
2831
2842
  _animateViewport(ms) {
2832
2843
  if (!this._viewportAnimation) return;
2833
2844
  this._viewportAnimation.elapsed += ms;
2834
- const { elapsed, easing, duration, start, end } = this._viewportAnimation;
2845
+ const { elapsed, easing, duration, start, end, opts } = this._viewportAnimation;
2835
2846
  if (elapsed > duration) {
2836
2847
  this.off("tick", this._animateViewport);
2837
2848
  this._viewportAnimation = null;
2838
- this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width));
2849
+ this._setCamera(
2850
+ new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width),
2851
+ opts
2852
+ );
2839
2853
  return;
2840
2854
  }
2841
2855
  const remaining = duration - elapsed;
@@ -2873,7 +2887,8 @@ class Editor extends EventEmitter {
2873
2887
  duration: duration / animationSpeed,
2874
2888
  easing,
2875
2889
  start: viewportPageBounds.clone(),
2876
- end: targetViewportPage.clone()
2890
+ end: targetViewportPage.clone(),
2891
+ opts: rest
2877
2892
  };
2878
2893
  this.once("stop-camera-animation", () => {
2879
2894
  this.off("tick", this._animateViewport);
@@ -2959,6 +2974,7 @@ class Editor extends EventEmitter {
2959
2974
  }
2960
2975
  const isOnSamePage = presence.currentPageId === this.getCurrentPageId();
2961
2976
  if (!isOnSamePage) {
2977
+ this.markHistoryStoppingPoint("change-page");
2962
2978
  this.setCurrentPage(presence.currentPageId);
2963
2979
  }
2964
2980
  if (opts && opts.animation && !isOnSamePage) {
@@ -3790,7 +3806,9 @@ class Editor extends EventEmitter {
3790
3806
  if (ids.length <= 0) return this;
3791
3807
  this.run(
3792
3808
  () => {
3793
- this.store.props.assets.remove?.(ids);
3809
+ Promise.resolve(this.store.props.assets.remove?.(ids)).catch(
3810
+ (err) => console.error("Error while removing assets from the asset store:", err)
3811
+ );
3794
3812
  this.store.remove(ids);
3795
3813
  },
3796
3814
  { history: "ignore" }
@@ -4172,7 +4190,7 @@ class Editor extends EventEmitter {
4172
4190
  if (shapes.length === 0) {
4173
4191
  return;
4174
4192
  }
4175
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
4193
+ const ids = toShapeIds(shapes);
4176
4194
  const freshShapes = compact(ids.map((id) => this.getShape(id)));
4177
4195
  if (freshShapes.length === 1) {
4178
4196
  const parentId = freshShapes[0].parentId;
@@ -4269,12 +4287,14 @@ class Editor extends EventEmitter {
4269
4287
  */
4270
4288
  getSelectedShapeAtPoint(point) {
4271
4289
  const selectedShapeIds = this.getSelectedShapeIds();
4272
- const margin = this.options.hitTestMargin / this.getZoomLevel();
4290
+ if (selectedShapeIds.length === 0) return void 0;
4291
+ const selectedShapeIdSet = new Set(selectedShapeIds);
4292
+ const margin = this.getHitTestMargin();
4273
4293
  const sortedShapes = this.getCurrentPageShapesSorted();
4274
4294
  for (let i = sortedShapes.length - 1; i >= 0; i--) {
4275
4295
  const shape = sortedShapes[i];
4276
4296
  if (shape.type === "group") continue;
4277
- if (!selectedShapeIds.includes(shape.id)) continue;
4297
+ if (!selectedShapeIdSet.has(shape.id)) continue;
4278
4298
  if (this.getShapeGeometry(shape).hitTestPoint(
4279
4299
  this.getPointInShapeSpace(shape, point),
4280
4300
  margin,
@@ -4372,7 +4392,6 @@ class Editor extends EventEmitter {
4372
4392
  }
4373
4393
  return inMarginClosestToEdgeHit || shape;
4374
4394
  } else {
4375
- if (this.getShapePageBounds(shape).contains(viewportPageBounds)) continue;
4376
4395
  if (hitInside ? (
4377
4396
  // On hitInside, the distance will be negative for hits inside
4378
4397
  // If the distance is positive, check against the outer margin
@@ -4389,6 +4408,7 @@ class Editor extends EventEmitter {
4389
4408
  inMarginClosestToEdgeHit = shape;
4390
4409
  }
4391
4410
  } else if (!inMarginClosestToEdgeHit) {
4411
+ if (this.getShapePageBounds(shape).contains(viewportPageBounds)) continue;
4392
4412
  const { area } = geometry;
4393
4413
  if (area < inHollowSmallestArea) {
4394
4414
  inHollowSmallestArea = area;
@@ -4399,6 +4419,9 @@ class Editor extends EventEmitter {
4399
4419
  }
4400
4420
  } else {
4401
4421
  if (distance < this.getHitTestMargin()) {
4422
+ if (inMarginClosestToEdgeHit && inMarginClosestToEdgeDistance <= distance) {
4423
+ return inMarginClosestToEdgeHit;
4424
+ }
4402
4425
  return shape;
4403
4426
  }
4404
4427
  }
@@ -4675,7 +4698,7 @@ class Editor extends EventEmitter {
4675
4698
  * @public
4676
4699
  */
4677
4700
  reparentShapes(shapes, parentId, insertIndex) {
4678
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
4701
+ const ids = toShapeIds(shapes);
4679
4702
  if (ids.length === 0) return this;
4680
4703
  const changes = [];
4681
4704
  const parentTransform = isPageId(parentId) ? Mat.Identity() : this.getShapePageTransform(parentId);
@@ -4904,6 +4927,7 @@ class Editor extends EventEmitter {
4904
4927
  * binding, but the `type`, `toId`, and `fromId` must all be provided.
4905
4928
  */
4906
4929
  createBindings(partials) {
4930
+ if (this.getIsReadonly()) return this;
4907
4931
  const bindings = [];
4908
4932
  for (const partial of partials) {
4909
4933
  const fromShape = this.getShape(partial.fromId);
@@ -4938,6 +4962,7 @@ class Editor extends EventEmitter {
4938
4962
  * binding is skipped. The changes from the partial are merged into the existing record.
4939
4963
  */
4940
4964
  updateBindings(partials) {
4965
+ if (this.getIsReadonly()) return this;
4941
4966
  const updated = [];
4942
4967
  for (const partial of partials) {
4943
4968
  if (!partial) continue;
@@ -4966,6 +4991,16 @@ class Editor extends EventEmitter {
4966
4991
  * Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
4967
4992
  */
4968
4993
  deleteBindings(bindings, { isolateShapes = false } = {}) {
4994
+ if (this.getIsReadonly()) return this;
4995
+ return this._deleteBindings(bindings, { isolateShapes });
4996
+ }
4997
+ /**
4998
+ * Unguarded so that withIsolatedShapes can transiently isolate shapes for copy and export in
4999
+ * readonly mode; the public deleteBindings is what readonly blocks.
5000
+ *
5001
+ * @internal
5002
+ */
5003
+ _deleteBindings(bindings, { isolateShapes = false } = {}) {
4969
5004
  const ids = bindings.map((binding) => typeof binding === "string" ? binding : binding.id);
4970
5005
  if (isolateShapes) {
4971
5006
  this.store.atomic(() => {
@@ -5024,7 +5059,7 @@ class Editor extends EventEmitter {
5024
5059
  * @param opts - The options for the rotation.
5025
5060
  */
5026
5061
  rotateShapesBy(shapes, delta, opts) {
5027
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5062
+ const ids = toShapeIds(shapes);
5028
5063
  if (ids.length <= 0) return this;
5029
5064
  const snapshot = getRotationSnapshot({ editor: this, ids });
5030
5065
  if (!snapshot) return this;
@@ -5061,6 +5096,14 @@ class Editor extends EventEmitter {
5061
5096
  }
5062
5097
  return workingShape;
5063
5098
  }
5099
+ // Counter-rotates the page-space delta into the parent's space; without that a child of a rotated
5100
+ // frame or group would move along its parent's axes instead of the page's.
5101
+ // todo: a shape laid out together with its own parent moves twice, once with the parent and once
5102
+ // on its own; the layout commands should skip shapes whose ancestors are also being laid out
5103
+ getChangesToTranslateShapeByPageDelta(shape, pageDelta) {
5104
+ const localDelta = Vec.From(pageDelta).rot(-this.getShapeParentTransform(shape).rotation());
5105
+ return this.getChangesToTranslateShape(shape, localDelta.add(shape));
5106
+ }
5064
5107
  /**
5065
5108
  * Move shapes by a delta.
5066
5109
  *
@@ -5073,14 +5116,12 @@ class Editor extends EventEmitter {
5073
5116
  * @param offset - The offset to apply to the shapes.
5074
5117
  */
5075
5118
  nudgeShapes(shapes, offset) {
5076
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5119
+ const ids = toShapeIds(shapes);
5077
5120
  if (ids.length <= 0) return this;
5078
5121
  const changes = [];
5079
5122
  for (const id of ids) {
5080
5123
  const shape = this.getShape(id);
5081
- const localDelta = Vec.From(offset);
5082
- localDelta.rot(-this.getShapeParentTransform(shape).rotation());
5083
- changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)));
5124
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, offset));
5084
5125
  }
5085
5126
  this.updateShapes(changes);
5086
5127
  return this;
@@ -5101,7 +5142,7 @@ class Editor extends EventEmitter {
5101
5142
  */
5102
5143
  duplicateShapes(shapes, offset) {
5103
5144
  this.run(() => {
5104
- const _ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5145
+ const _ids = toShapeIds(shapes);
5105
5146
  const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids);
5106
5147
  if (ids.length <= 0) return this;
5107
5148
  const initialIds = new Set(ids);
@@ -5210,7 +5251,7 @@ class Editor extends EventEmitter {
5210
5251
  * @public
5211
5252
  */
5212
5253
  moveShapesToPage(shapes, pageId) {
5213
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5254
+ const ids = toShapeIds(shapes);
5214
5255
  if (ids.length === 0) return this;
5215
5256
  if (this.getIsReadonly()) return this;
5216
5257
  const currentPageId = this.getCurrentPageId();
@@ -5246,7 +5287,7 @@ class Editor extends EventEmitter {
5246
5287
  * @public
5247
5288
  */
5248
5289
  toggleLock(shapes) {
5249
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5290
+ const ids = toShapeIds(shapes);
5250
5291
  if (this.getIsReadonly() || ids.length === 0) return this;
5251
5292
  let allLocked = true;
5252
5293
  const shapesToToggle = [];
@@ -5285,7 +5326,7 @@ class Editor extends EventEmitter {
5285
5326
  * @public
5286
5327
  */
5287
5328
  sendToBack(shapes) {
5288
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5329
+ const ids = toShapeIds(shapes);
5289
5330
  const changes = getReorderingShapesChanges(this, "toBack", ids, {
5290
5331
  considerAllShapes: true
5291
5332
  });
@@ -5315,7 +5356,7 @@ class Editor extends EventEmitter {
5315
5356
  * @public
5316
5357
  */
5317
5358
  sendBackward(shapes, opts = {}) {
5318
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5359
+ const ids = toShapeIds(shapes);
5319
5360
  const changes = getReorderingShapesChanges(this, "backward", ids, opts);
5320
5361
  if (changes) this.updateShapes(changes);
5321
5362
  return this;
@@ -5343,7 +5384,7 @@ class Editor extends EventEmitter {
5343
5384
  * @public
5344
5385
  */
5345
5386
  bringForward(shapes, opts = {}) {
5346
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5387
+ const ids = toShapeIds(shapes);
5347
5388
  const changes = getReorderingShapesChanges(this, "forward", ids, opts);
5348
5389
  if (changes) this.updateShapes(changes);
5349
5390
  return this;
@@ -5362,7 +5403,7 @@ class Editor extends EventEmitter {
5362
5403
  * @public
5363
5404
  */
5364
5405
  bringToFront(shapes) {
5365
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5406
+ const ids = toShapeIds(shapes);
5366
5407
  const changes = getReorderingShapesChanges(this, "toFront", ids);
5367
5408
  if (changes) this.updateShapes(changes);
5368
5409
  return this;
@@ -5374,7 +5415,7 @@ class Editor extends EventEmitter {
5374
5415
  * @internal
5375
5416
  */
5376
5417
  getShapeClusters(shapes, type, opts) {
5377
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5418
+ const ids = toShapeIds(shapes);
5378
5419
  let freshShapes = compact(ids.map((id) => this.getShape(id)));
5379
5420
  if (opts?.filterAxisAligned) {
5380
5421
  freshShapes = freshShapes.filter((s) => {
@@ -5399,7 +5440,7 @@ class Editor extends EventEmitter {
5399
5440
  const shapesMovingTogether = [shape];
5400
5441
  const boundsOfShapesMovingTogether = [shapePageBounds];
5401
5442
  this.collectShapesViaArrowBindings({
5402
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5443
+ bindings: this.getBindingsInvolvingShape(shape.id, "arrow"),
5403
5444
  initialShapes: freshShapes,
5404
5445
  resultShapes: shapesMovingTogether,
5405
5446
  resultBounds: boundsOfShapesMovingTogether,
@@ -5455,7 +5496,7 @@ class Editor extends EventEmitter {
5455
5496
  */
5456
5497
  flipShapes(shapes, operation) {
5457
5498
  if (this.getIsReadonly()) return this;
5458
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5499
+ const ids = toShapeIds(shapes);
5459
5500
  const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)));
5460
5501
  for (const shape of shapesToFlipFirstPass) {
5461
5502
  if (this.isShapeOfType(shape, "group")) {
@@ -5529,21 +5570,7 @@ class Editor extends EventEmitter {
5529
5570
  const { clusters: shapeClustersToStack } = this.getShapeClusters(shapes, "stack");
5530
5571
  const len = shapeClustersToStack.length;
5531
5572
  if (_gap === 0 && len < 3 || len < 2) return this;
5532
- let val;
5533
- let min;
5534
- let max;
5535
- let dim;
5536
- if (operation === "horizontal") {
5537
- val = "x";
5538
- min = "minX";
5539
- max = "maxX";
5540
- dim = "width";
5541
- } else {
5542
- val = "y";
5543
- min = "minY";
5544
- max = "maxY";
5545
- dim = "height";
5546
- }
5573
+ const { val, min, max, dim } = AXIS[operation];
5547
5574
  let shapeGap = 0;
5548
5575
  shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min]);
5549
5576
  if (_gap === 0) {
@@ -5582,13 +5609,7 @@ class Editor extends EventEmitter {
5582
5609
  const delta = new Vec();
5583
5610
  delta[val] = v + shapeGap - pageBounds[val];
5584
5611
  for (const shape of shapes2) {
5585
- const shapeDelta = delta.clone();
5586
- const parent = this.getShapeParent(shape);
5587
- if (parent) {
5588
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5589
- }
5590
- shapeDelta.add(shape);
5591
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5612
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5592
5613
  }
5593
5614
  v += pageBounds[dim] + shapeGap;
5594
5615
  }
@@ -5668,13 +5689,7 @@ class Editor extends EventEmitter {
5668
5689
  for (const { shapes: shapes2, pageBounds, nextPageBounds } of shapeClustersToPack) {
5669
5690
  const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta);
5670
5691
  for (const shape of shapes2) {
5671
- const shapeDelta = delta.clone();
5672
- const parent = this.getShapeParent(shape);
5673
- if (parent) {
5674
- shapeDelta.rot(-this.getShapeParentTransform(shape).rotation());
5675
- }
5676
- shapeDelta.add(shape);
5677
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5692
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5678
5693
  }
5679
5694
  }
5680
5695
  if (changes.length) {
@@ -5734,13 +5749,7 @@ class Editor extends EventEmitter {
5734
5749
  }
5735
5750
  }
5736
5751
  for (const shape of shapes2) {
5737
- const shapeDelta = delta.clone();
5738
- const parent = this.getShapeParent(shape);
5739
- if (parent) {
5740
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5741
- }
5742
- shapeDelta.add(shape);
5743
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5752
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5744
5753
  }
5745
5754
  });
5746
5755
  this.updateShapes(changes);
@@ -5764,27 +5773,13 @@ class Editor extends EventEmitter {
5764
5773
  if (this.getIsReadonly()) return this;
5765
5774
  const { clusters: shapeClustersToDistribute } = this.getShapeClusters(shapes, "distribute");
5766
5775
  if (shapeClustersToDistribute.length < 3) return this;
5767
- let val;
5768
- let min;
5769
- let max;
5770
- let dim;
5771
- if (operation === "horizontal") {
5772
- val = "x";
5773
- min = "minX";
5774
- max = "maxX";
5775
- dim = "width";
5776
- } else {
5777
- val = "y";
5778
- min = "minY";
5779
- max = "maxY";
5780
- dim = "height";
5781
- }
5776
+ const { val, min, max, dim } = AXIS[operation];
5782
5777
  const changes = [];
5783
5778
  const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0];
5784
5779
  const last2 = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0];
5785
5780
  if (first === last2) {
5786
5781
  const excludedShapeIds = new Set(first.shapes.map((s) => s.id));
5787
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5782
+ const ids = toShapeIds(shapes);
5788
5783
  return this.distributeShapes(
5789
5784
  ids.filter((id) => !excludedShapeIds.has(id)),
5790
5785
  operation
@@ -5808,13 +5803,7 @@ class Editor extends EventEmitter {
5808
5803
  delta[val] = last2.pageBounds[max] - pageBounds[max] - 1;
5809
5804
  }
5810
5805
  for (const shape of shapes2) {
5811
- const shapeDelta = delta.clone();
5812
- const parent = this.getShapeParent(shape);
5813
- if (parent) {
5814
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5815
- }
5816
- shapeDelta.add(shape);
5817
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5806
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5818
5807
  }
5819
5808
  v += pageBounds[dim] + gap;
5820
5809
  }
@@ -5844,32 +5833,17 @@ class Editor extends EventEmitter {
5844
5833
  );
5845
5834
  if (shapeClustersToStretch.length < 2) return this;
5846
5835
  const commonBounds = Box.Common(allBounds);
5847
- let val;
5848
- let min;
5849
- let dim;
5850
- if (operation === "horizontal") {
5851
- val = "x";
5852
- min = "minX";
5853
- dim = "width";
5854
- } else {
5855
- val = "y";
5856
- min = "minY";
5857
- dim = "height";
5858
- }
5836
+ const { val, min, dim } = AXIS[operation];
5859
5837
  this.run(() => {
5860
5838
  shapeClustersToStretch.forEach(({ shapes: shapes2, pageBounds }) => {
5861
- const localOffset = new Vec();
5862
- localOffset[val] = commonBounds[min] - pageBounds[min];
5839
+ const pageOffset = new Vec();
5840
+ pageOffset[val] = commonBounds[min] - pageBounds[min];
5863
5841
  const scaleOrigin = pageBounds.center.clone();
5864
5842
  scaleOrigin[val] = commonBounds[min];
5865
5843
  const scale = new Vec(1, 1);
5866
5844
  scale[val] = commonBounds[dim] / pageBounds[dim];
5867
5845
  for (const shape of shapes2) {
5868
- const shapeLocalOffset = localOffset.clone();
5869
- shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation());
5870
- shapeLocalOffset.add(shape);
5871
- const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5872
- this.updateShape(changes);
5846
+ this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset));
5873
5847
  this.resizeShape(shape.id, scale, {
5874
5848
  initialBounds: this.getShapeGeometry(shape).bounds,
5875
5849
  scaleOrigin,
@@ -5912,7 +5886,7 @@ class Editor extends EventEmitter {
5912
5886
  const scaleY = targetBounds.height / commonBounds.height;
5913
5887
  const scale = new Vec(scaleX, scaleY);
5914
5888
  shapeClusters.forEach(({ shapes: shapes2, pageBounds }) => {
5915
- const localOffset = new Vec(
5889
+ const pageOffset = new Vec(
5916
5890
  targetBounds.minX - commonBounds.minX + (pageBounds.minX - commonBounds.minX) * (scaleX - 1),
5917
5891
  targetBounds.minY - commonBounds.minY + (pageBounds.minY - commonBounds.minY) * (scaleY - 1)
5918
5892
  );
@@ -5921,11 +5895,7 @@ class Editor extends EventEmitter {
5921
5895
  targetBounds.minY + (pageBounds.minY - commonBounds.minY) * scaleY
5922
5896
  );
5923
5897
  for (const shape of shapes2) {
5924
- const shapeLocalOffset = localOffset.clone();
5925
- shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation());
5926
- shapeLocalOffset.add(shape);
5927
- const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5928
- this.updateShape(changes);
5898
+ this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset));
5929
5899
  this.resizeShape(shape.id, scale, {
5930
5900
  initialBounds: this.getShapeGeometry(shape).bounds,
5931
5901
  scaleOrigin,
@@ -6344,12 +6314,17 @@ class Editor extends EventEmitter {
6344
6314
  let remaining = duration;
6345
6315
  let t;
6346
6316
  const animations = [];
6317
+ const ignoreShapeLock = this._shouldIgnoreShapeLock;
6347
6318
  let partial, result;
6348
6319
  for (let i = 0, n = partials.length; i < n; i++) {
6349
6320
  partial = partials[i];
6350
6321
  if (!partial) continue;
6351
6322
  const shape = this.getShape(partial.id);
6352
6323
  if (!shape) continue;
6324
+ const unlocks = shape.isLocked && Object.hasOwn(partial, "isLocked") && !partial.isLocked;
6325
+ if (!ignoreShapeLock && !unlocks && this.isShapeOrAncestorLocked(shape)) {
6326
+ continue;
6327
+ }
6353
6328
  result = {
6354
6329
  start: structuredClone(shape),
6355
6330
  end: applyPartialToRecordWithProps(structuredClone(shape), partial)
@@ -6365,7 +6340,7 @@ class Editor extends EventEmitter {
6365
6340
  (p) => p && animatingShapes2.get(p.id) === animationId
6366
6341
  );
6367
6342
  if (partialsToUpdate.length) {
6368
- this.updateShapes(partialsToUpdate);
6343
+ this.run(() => this.updateShapes(partialsToUpdate), { ignoreShapeLock });
6369
6344
  }
6370
6345
  this.off("tick", handleTick);
6371
6346
  return;
@@ -6398,13 +6373,13 @@ class Editor extends EventEmitter {
6398
6373
  throw Error("Editor.groupShapes: must provide an array of shapes or shape ids");
6399
6374
  }
6400
6375
  if (this.getIsReadonly()) return this;
6401
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6402
- if (ids.length <= 1) return this;
6376
+ const ids = toShapeIds(shapes);
6403
6377
  const shapesToGroup = compact(
6404
6378
  (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
6405
6379
  (id) => this.getShape(id)
6406
6380
  )
6407
6381
  );
6382
+ if (shapesToGroup.length <= 1) return this;
6408
6383
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id);
6409
6384
  const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)));
6410
6385
  const pageBounds = Box.Common(childBounds);
@@ -6413,8 +6388,11 @@ class Editor extends EventEmitter {
6413
6388
  }
6414
6389
  const { x, y } = pageBounds.point;
6415
6390
  const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId();
6416
- if (this.getCurrentToolId() !== "select") return this;
6417
- if (!this.isIn("select.idle")) {
6391
+ if (!this.canCreateShapes([groupId])) {
6392
+ alertMaxShapes(this);
6393
+ return this;
6394
+ }
6395
+ if (this.isIn("select") && !this.isIn("select.idle")) {
6418
6396
  this.cancel();
6419
6397
  }
6420
6398
  const shapesWithRootParent = shapesToGroup.filter((shape) => shape.parentId === parentId).sort(sortByIndex);
@@ -6442,15 +6420,14 @@ class Editor extends EventEmitter {
6442
6420
  ungroupShapes(shapes, opts = {}) {
6443
6421
  if (this.getIsReadonly()) return this;
6444
6422
  const { select = true } = opts;
6445
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6423
+ const ids = toShapeIds(shapes);
6446
6424
  const shapesToUngroup = compact(
6447
6425
  (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
6448
6426
  (id) => this.getShape(id)
6449
6427
  )
6450
6428
  );
6451
6429
  if (shapesToUngroup.length === 0) return this;
6452
- if (this.getCurrentToolId() !== "select") return this;
6453
- if (!this.isIn("select.idle")) {
6430
+ if (this.isIn("select") && !this.isIn("select.idle")) {
6454
6431
  this.cancel();
6455
6432
  }
6456
6433
  const idsToSelect = /* @__PURE__ */ new Set();
@@ -6464,16 +6441,16 @@ class Editor extends EventEmitter {
6464
6441
  });
6465
6442
  if (groups.length === 0) return this;
6466
6443
  this.run(() => {
6467
- let group;
6468
6444
  for (let i = 0, n = groups.length; i < n; i++) {
6469
- group = groups[i];
6445
+ const group = this.getShape(groups[i].id);
6446
+ if (!group) continue;
6470
6447
  const childIds = this.getSortedChildIdsForParent(group.id);
6471
6448
  for (let j = 0, n2 = childIds.length; j < n2; j++) {
6472
6449
  idsToSelect.add(childIds[j]);
6473
6450
  }
6474
6451
  this.reparentShapes(childIds, group.parentId, group.index);
6475
6452
  }
6476
- this.deleteShapes(groups.map((group2) => group2.id));
6453
+ this.deleteShapes(groups.map((group) => group.id));
6477
6454
  if (select) {
6478
6455
  this.select(...idsToSelect);
6479
6456
  }
@@ -6554,14 +6531,14 @@ class Editor extends EventEmitter {
6554
6531
  }
6555
6532
  /** @internal */
6556
6533
  _getUnlockedShapeIds(ids) {
6557
- return ids.filter((id) => !this.getShape(id)?.isLocked);
6534
+ return ids.filter((id) => !this.isShapeOrAncestorLocked(id));
6558
6535
  }
6559
6536
  deleteShapes(_ids) {
6560
6537
  if (this.getIsReadonly()) return this;
6561
6538
  if (!Array.isArray(_ids)) {
6562
6539
  throw Error("Editor.deleteShapes: must provide an array of shapes or shapeIds");
6563
6540
  }
6564
- const shapeIds = typeof _ids[0] === "string" ? _ids : _ids.map((s) => s.id);
6541
+ const shapeIds = toShapeIds(_ids);
6565
6542
  const shapeIdsToDelete = this._shouldIgnoreShapeLock ? shapeIds : this._getUnlockedShapeIds(shapeIds);
6566
6543
  if (shapeIdsToDelete.length === 0) return this;
6567
6544
  const allShapeIdsToDelete = new Set(shapeIdsToDelete);
@@ -6906,7 +6883,7 @@ class Editor extends EventEmitter {
6906
6883
  * @public
6907
6884
  */
6908
6885
  getContentFromCurrentPage(shapes) {
6909
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6886
+ const ids = toShapeIds(shapes);
6910
6887
  if (!ids) return;
6911
6888
  if (ids.length === 0) return;
6912
6889
  const shapeIds = this.getShapeAndDescendantIds(ids);
@@ -6974,21 +6951,29 @@ class Editor extends EventEmitter {
6974
6951
  await Promise.allSettled(
6975
6952
  content.assets.map(async (asset) => {
6976
6953
  if ((asset.type === "image" || asset.type === "video") && !asset.props.src?.startsWith("data:image") && !asset.props.src?.startsWith("data:video") && !asset.props.src?.startsWith("http")) {
6977
- const assetWithDataUrl = structuredClone(asset);
6978
- const objectUrl = await this.store.props.assets.resolve(asset, {
6979
- screenScale: 1,
6980
- steppedScreenScale: 1,
6981
- dpr: 1,
6982
- networkEffectiveType: null,
6983
- shouldResolveToOriginal: true
6984
- });
6985
- assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(
6986
- await fetch(objectUrl).then((r) => r.blob())
6987
- );
6988
- assets.push(assetWithDataUrl);
6989
- } else {
6990
- assets.push(asset);
6954
+ try {
6955
+ const objectUrl = await this.store.props.assets.resolve(asset, {
6956
+ screenScale: 1,
6957
+ steppedScreenScale: 1,
6958
+ dpr: 1,
6959
+ networkEffectiveType: null,
6960
+ shouldResolveToOriginal: true
6961
+ });
6962
+ if (objectUrl) {
6963
+ const response = await fetch(objectUrl);
6964
+ if (response.ok) {
6965
+ const assetWithDataUrl = structuredClone(asset);
6966
+ assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(await response.blob());
6967
+ assets.push(assetWithDataUrl);
6968
+ return;
6969
+ }
6970
+ console.warn(`Could not inline asset ${asset.id}: fetch returned ${response.status}`);
6971
+ }
6972
+ } catch (err) {
6973
+ console.warn(`Could not inline asset ${asset.id}`, err);
6974
+ }
6991
6975
  }
6976
+ assets.push(asset);
6992
6977
  })
6993
6978
  );
6994
6979
  content.assets = assets;
@@ -7282,7 +7267,7 @@ class Editor extends EventEmitter {
7282
7267
  * @public
7283
7268
  */
7284
7269
  async getSvgElement(shapes, opts = {}) {
7285
- const ids = shapes.length === 0 ? this.getCurrentPageShapeIdsSorted() : typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
7270
+ const ids = shapes.length === 0 ? this.getCurrentPageShapeIdsSorted() : toShapeIds(shapes);
7286
7271
  if (ids.length === 0) return void 0;
7287
7272
  await this.fonts.loadRequiredFontsForCurrentPage(this.options.maxFontsToLoadBeforeRender);
7288
7273
  return exportToSvg(this, ids, opts);
@@ -7878,22 +7863,22 @@ class Editor extends EventEmitter {
7878
7863
  const releaseCtrl = this._ctrlKeyTimeout !== -1;
7879
7864
  const releaseMeta = this._metaKeyTimeout !== -1;
7880
7865
  if (releaseShift) {
7881
- clearTimeout(this._shiftKeyTimeout);
7866
+ this.timers.clearTimeout(this._shiftKeyTimeout);
7882
7867
  this._shiftKeyTimeout = -1;
7883
7868
  this.inputs.setShiftKey(false);
7884
7869
  }
7885
7870
  if (releaseAlt) {
7886
- clearTimeout(this._altKeyTimeout);
7871
+ this.timers.clearTimeout(this._altKeyTimeout);
7887
7872
  this._altKeyTimeout = -1;
7888
7873
  this.inputs.setAltKey(false);
7889
7874
  }
7890
7875
  if (releaseCtrl) {
7891
- clearTimeout(this._ctrlKeyTimeout);
7876
+ this.timers.clearTimeout(this._ctrlKeyTimeout);
7892
7877
  this._ctrlKeyTimeout = -1;
7893
7878
  this.inputs.setCtrlKey(false);
7894
7879
  }
7895
7880
  if (releaseMeta) {
7896
- clearTimeout(this._metaKeyTimeout);
7881
+ this.timers.clearTimeout(this._metaKeyTimeout);
7897
7882
  this._metaKeyTimeout = -1;
7898
7883
  this.inputs.setMetaKey(false);
7899
7884
  }
@@ -8007,28 +7992,28 @@ class Editor extends EventEmitter {
8007
7992
  return;
8008
7993
  }
8009
7994
  if (info.shiftKey) {
8010
- clearTimeout(this._shiftKeyTimeout);
7995
+ this.timers.clearTimeout(this._shiftKeyTimeout);
8011
7996
  this._shiftKeyTimeout = -1;
8012
7997
  inputs.setShiftKey(true);
8013
7998
  } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
8014
7999
  this._shiftKeyTimeout = this.timers.setTimeout(this._releaseShiftKey, 150);
8015
8000
  }
8016
8001
  if (info.altKey) {
8017
- clearTimeout(this._altKeyTimeout);
8002
+ this.timers.clearTimeout(this._altKeyTimeout);
8018
8003
  this._altKeyTimeout = -1;
8019
8004
  inputs.setAltKey(true);
8020
8005
  } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
8021
8006
  this._altKeyTimeout = this.timers.setTimeout(this._releaseAltKey, 150);
8022
8007
  }
8023
8008
  if (info.ctrlKey) {
8024
- clearTimeout(this._ctrlKeyTimeout);
8009
+ this.timers.clearTimeout(this._ctrlKeyTimeout);
8025
8010
  this._ctrlKeyTimeout = -1;
8026
8011
  inputs.setCtrlKey(true);
8027
8012
  } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
8028
8013
  this._ctrlKeyTimeout = this.timers.setTimeout(this._releaseCtrlKey, 150);
8029
8014
  }
8030
8015
  if (info.metaKey && info.name !== "key_up") {
8031
- clearTimeout(this._metaKeyTimeout);
8016
+ this.timers.clearTimeout(this._metaKeyTimeout);
8032
8017
  this._metaKeyTimeout = -1;
8033
8018
  inputs.setMetaKey(true);
8034
8019
  } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
@@ -8042,8 +8027,7 @@ class Editor extends EventEmitter {
8042
8027
  const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture();
8043
8028
  switch (type) {
8044
8029
  case "pinch": {
8045
- if (cameraOptions.isLocked) return;
8046
- clearTimeout(this._longPressTimeout);
8030
+ this.timers.clearTimeout(this._longPressTimeout);
8047
8031
  this.inputs.updateFromEvent(info);
8048
8032
  switch (info.name) {
8049
8033
  case "pinch_start": {
@@ -8064,6 +8048,7 @@ class Editor extends EventEmitter {
8064
8048
  }
8065
8049
  case "pinch": {
8066
8050
  if (!inputs.getIsPinching()) return;
8051
+ if (cameraOptions.isLocked) return;
8067
8052
  const {
8068
8053
  point: { z = 1 },
8069
8054
  delta: { x: dx, y: dy }
@@ -8119,7 +8104,7 @@ class Editor extends EventEmitter {
8119
8104
  const { panSpeed, zoomSpeed } = cameraOptions;
8120
8105
  let wheelBehavior = cameraOptions.wheelBehavior;
8121
8106
  const inputMode = this.user.getUserPreferences().inputMode;
8122
- if (inputMode !== null) {
8107
+ if (inputMode !== null && wheelBehavior !== "none") {
8123
8108
  wheelBehavior = inputMode === "trackpad" ? "pan" : "zoom";
8124
8109
  }
8125
8110
  if (wheelBehavior !== "none") {
@@ -8171,12 +8156,12 @@ class Editor extends EventEmitter {
8171
8156
  }
8172
8157
  case "pointer": {
8173
8158
  if (inputs.getIsPinching()) return;
8174
- this.inputs.updateFromEvent(info);
8175
8159
  const { isPen } = info;
8176
8160
  const { isPenMode } = instanceState;
8161
+ if (isPenMode && !isPen) return;
8162
+ this.inputs.updateFromEvent(info);
8177
8163
  switch (info.name) {
8178
8164
  case "pointer_down": {
8179
- if (isPenMode && !isPen) return;
8180
8165
  this._releaseDebouncedModifiers();
8181
8166
  if (!this.inputs.getIsPanning()) {
8182
8167
  this._longPressTimeout = this.timers.setTimeout(() => {
@@ -8200,11 +8185,12 @@ class Editor extends EventEmitter {
8200
8185
  inputs.buttons.add(info.button);
8201
8186
  inputs.setIsPointing(true);
8202
8187
  inputs.setIsDragging(false);
8188
+ this.stopCameraAnimation();
8203
8189
  if (!isPenMode && info.isPenDirect) {
8204
8190
  this.updateInstanceState({ isPenMode: true });
8205
8191
  this.interrupt();
8206
8192
  }
8207
- if (info.button === STYLUS_ERASER_BUTTON) {
8193
+ if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant("eraser")) {
8208
8194
  this._restoreToolId = this.getCurrentToolId();
8209
8195
  this.complete();
8210
8196
  this.setCurrentTool("eraser");
@@ -8213,10 +8199,10 @@ class Editor extends EventEmitter {
8213
8199
  this._prevCursor = this.getInstanceState().cursor.type;
8214
8200
  }
8215
8201
  this.inputs.setIsPanning(true);
8216
- clearTimeout(this._longPressTimeout);
8202
+ this.timers.clearTimeout(this._longPressTimeout);
8217
8203
  } else if (info.button === RIGHT_MOUSE_BUTTON && this.options.rightClickPanning) {
8218
8204
  this.inputs.setIsRightPointing(true);
8219
- clearTimeout(this._longPressTimeout);
8205
+ this.timers.clearTimeout(this._longPressTimeout);
8220
8206
  return this;
8221
8207
  }
8222
8208
  if (this.inputs.getIsPanning()) {
@@ -8227,7 +8213,6 @@ class Editor extends EventEmitter {
8227
8213
  break;
8228
8214
  }
8229
8215
  case "pointer_move": {
8230
- if (!isPen && isPenMode) return;
8231
8216
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera());
8232
8217
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
8233
8218
  const currentScreenPoint = this.inputs.getCurrentScreenPoint();
@@ -8258,16 +8243,15 @@ class Editor extends EventEmitter {
8258
8243
  }
8259
8244
  if (inputs.getIsPointing() && !inputs.getIsDragging() && Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) * this.getZoomLevel() > (instanceState.isCoarsePointer ? this.options.coarseDragDistanceSquared : this.options.dragDistanceSquared) / cz) {
8260
8245
  inputs.setIsDragging(true);
8261
- clearTimeout(this._longPressTimeout);
8246
+ this.timers.clearTimeout(this._longPressTimeout);
8262
8247
  }
8263
8248
  break;
8264
8249
  }
8265
8250
  case "pointer_up": {
8266
8251
  inputs.setIsDragging(false);
8267
8252
  inputs.setIsPointing(false);
8268
- clearTimeout(this._longPressTimeout);
8253
+ this.timers.clearTimeout(this._longPressTimeout);
8269
8254
  inputs.buttons.delete(info.button);
8270
- if (instanceState.isPenMode && !isPen) return;
8271
8255
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
8272
8256
  this.inputs.setIsRightPointing(false);
8273
8257
  this._selectedShapeIdsAtPointerDown = [];
@@ -8323,7 +8307,7 @@ class Editor extends EventEmitter {
8323
8307
  });
8324
8308
  }
8325
8309
  } else {
8326
- if (info.button === STYLUS_ERASER_BUTTON) {
8310
+ if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant("eraser")) {
8327
8311
  this.complete();
8328
8312
  this.setCurrentTool(this._restoreToolId);
8329
8313
  }
@@ -8336,8 +8320,8 @@ class Editor extends EventEmitter {
8336
8320
  break;
8337
8321
  }
8338
8322
  case "keyboard": {
8339
- if (info.key === "ShiftRight") info.key = "ShiftLeft";
8340
- if (info.key === "AltRight") info.key = "AltLeft";
8323
+ if (info.code === "ShiftRight") info.code = "ShiftLeft";
8324
+ if (info.code === "AltRight") info.code = "AltLeft";
8341
8325
  if (info.code === "ControlRight") info.code = "ControlLeft";
8342
8326
  if (info.code === "MetaRight") info.code = "MetaLeft";
8343
8327
  switch (info.name) {
@@ -8350,7 +8334,7 @@ class Editor extends EventEmitter {
8350
8334
  }
8351
8335
  this.inputs.setIsPanning(true);
8352
8336
  this.inputs.setIsSpacebarPanning(true);
8353
- clearTimeout(this._longPressTimeout);
8337
+ this.timers.clearTimeout(this._longPressTimeout);
8354
8338
  this.setCursor({
8355
8339
  type: this.inputs.getIsPointing() ? "grabbing" : "grab",
8356
8340
  rotation: 0
@@ -8376,7 +8360,7 @@ class Editor extends EventEmitter {
8376
8360
  break;
8377
8361
  }
8378
8362
  }
8379
- if (offset) {
8363
+ if (offset && !cameraOptions.isLocked) {
8380
8364
  const bounds = this.getViewportPageBounds();
8381
8365
  const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }));
8382
8366
  this._animateToViewport(next, { animation: { duration: 320 } });
@@ -8416,7 +8400,7 @@ class Editor extends EventEmitter {
8416
8400
  this.setCurrentTool("select");
8417
8401
  }
8418
8402
  const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID);
8419
- if (info.isPen === isPenMode) {
8403
+ if (!isPenMode || info.isPen) {
8420
8404
  const clickInfo = this._clickManager.handlePointerEvent(info);
8421
8405
  if (info.name !== clickInfo.name) {
8422
8406
  this.root.handleEvent(info);
@@ -8438,7 +8422,7 @@ class Editor extends EventEmitter {
8438
8422
  maybeTrackPerformance(name) {
8439
8423
  if (debugFlags.measurePerformance.get()) {
8440
8424
  if (this.performanceTracker.isStarted()) {
8441
- clearTimeout(this.performanceTrackerTimeout);
8425
+ this.timers.clearTimeout(this.performanceTrackerTimeout);
8442
8426
  } else {
8443
8427
  this.performanceTracker.start(name);
8444
8428
  }
@@ -8707,6 +8691,9 @@ function pushShapeWithDescendants(editor, id, result) {
8707
8691
  pushShapeWithDescendants(editor, childIds[i], result);
8708
8692
  }
8709
8693
  }
8694
+ function toShapeIds(shapes) {
8695
+ return typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
8696
+ }
8710
8697
  function withIsolatedShapes(editor, shapeIds, callback) {
8711
8698
  let result;
8712
8699
  editor.run(
@@ -8729,7 +8716,7 @@ function withIsolatedShapes(editor, shapeIds, callback) {
8729
8716
  }
8730
8717
  }
8731
8718
  }
8732
- editor.deleteBindings([...bindingsToRemove], { isolateShapes: true });
8719
+ editor._deleteBindings([...bindingsToRemove], { isolateShapes: true });
8733
8720
  try {
8734
8721
  result = Result.ok(callback(bindingsWithBoth));
8735
8722
  } catch (error) {