@tldraw/editor 5.4.1 → 5.5.0-canary.003bf5951274

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 (200) hide show
  1. package/dist-cjs/index.d.ts +37 -14
  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/constants.js +2 -0
  14. package/dist-cjs/lib/constants.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +147 -157
  16. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  17. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
  18. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
  22. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
  24. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
  26. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
  27. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  28. package/dist-cjs/lib/exports/ExportDelay.js +10 -2
  29. package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
  30. package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
  31. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  32. package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
  33. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  34. package/dist-cjs/lib/exports/fetchCache.js +3 -0
  35. package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
  36. package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
  37. package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
  38. package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
  39. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  40. package/dist-cjs/lib/exports/parseCss.js +1 -1
  41. package/dist-cjs/lib/exports/parseCss.js.map +1 -1
  42. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  43. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
  44. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  45. package/dist-cjs/lib/hooks/useIdentity.js +6 -2
  46. package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
  47. package/dist-cjs/lib/license/LicenseManager.js +9 -1
  48. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  49. package/dist-cjs/lib/license/LicenseProvider.js +12 -4
  50. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  51. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  52. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  53. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  54. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  55. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  56. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  57. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -5
  58. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  59. package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
  60. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  61. package/dist-cjs/lib/utils/debug-flags.js +0 -6
  62. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  63. package/dist-cjs/lib/utils/deepLinks.js +2 -1
  64. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  65. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +22 -24
  66. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
  67. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +81 -40
  68. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  69. package/dist-cjs/version.js +3 -3
  70. package/dist-cjs/version.js.map +1 -1
  71. package/dist-esm/index.d.mts +37 -14
  72. package/dist-esm/index.mjs +4 -2
  73. package/dist-esm/index.mjs.map +2 -2
  74. package/dist-esm/lib/TldrawEditor.mjs +17 -13
  75. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  76. package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
  77. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  78. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
  79. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  80. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
  81. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  82. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  83. package/dist-esm/lib/constants.mjs +2 -0
  84. package/dist-esm/lib/constants.mjs.map +2 -2
  85. package/dist-esm/lib/editor/Editor.mjs +148 -157
  86. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  87. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
  88. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  89. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
  90. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
  92. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  93. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
  94. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
  95. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
  96. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
  97. package/dist-esm/lib/exports/ExportDelay.mjs +11 -3
  98. package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
  99. package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
  100. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  101. package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
  102. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  103. package/dist-esm/lib/exports/fetchCache.mjs +3 -0
  104. package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
  105. package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
  106. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
  107. package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
  108. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  109. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  110. package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
  111. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  112. package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
  113. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  114. package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
  115. package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
  116. package/dist-esm/lib/license/LicenseManager.mjs +9 -1
  117. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  118. package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
  119. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  120. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
  121. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  122. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  123. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  124. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  125. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  126. package/dist-esm/lib/primitives/geometry/Group2d.mjs +12 -6
  127. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  128. package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
  129. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  130. package/dist-esm/lib/utils/debug-flags.mjs +0 -6
  131. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  132. package/dist-esm/lib/utils/deepLinks.mjs +2 -1
  133. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  134. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +22 -24
  135. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
  136. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +81 -40
  137. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  138. package/dist-esm/version.mjs +3 -3
  139. package/dist-esm/version.mjs.map +1 -1
  140. package/editor.css +9 -1
  141. package/package.json +8 -11
  142. package/src/index.ts +5 -1
  143. package/src/lib/TldrawEditor.tsx +35 -21
  144. package/src/lib/components/MenuClickCapture.tsx +55 -4
  145. package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
  146. package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
  147. package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
  148. package/src/lib/constants.ts +8 -0
  149. package/src/lib/editor/Editor.test.ts +20 -0
  150. package/src/lib/editor/Editor.ts +228 -262
  151. package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
  152. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
  153. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
  154. package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
  155. package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
  156. package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
  157. package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
  158. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
  159. package/src/lib/editor/types/external-content.ts +5 -0
  160. package/src/lib/exports/ExportDelay.tsx +16 -4
  161. package/src/lib/exports/FontEmbedder.test.ts +99 -0
  162. package/src/lib/exports/FontEmbedder.ts +32 -11
  163. package/src/lib/exports/StyleEmbedder.ts +13 -4
  164. package/src/lib/exports/fetchCache.test.ts +22 -0
  165. package/src/lib/exports/fetchCache.ts +4 -0
  166. package/src/lib/exports/getSvgAsImage.ts +13 -5
  167. package/src/lib/exports/getSvgJsx.test.ts +43 -1
  168. package/src/lib/exports/getSvgJsx.tsx +28 -16
  169. package/src/lib/exports/parseCss.test.ts +4 -0
  170. package/src/lib/exports/parseCss.ts +2 -2
  171. package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
  172. package/src/lib/hooks/useCanvasEvents.ts +42 -0
  173. package/src/lib/hooks/useIdentity.tsx +12 -1
  174. package/src/lib/license/LicenseManager.ts +13 -1
  175. package/src/lib/license/LicenseProvider.test.tsx +76 -0
  176. package/src/lib/license/LicenseProvider.tsx +19 -5
  177. package/src/lib/primitives/geometry/Arc2d.test.ts +9 -0
  178. package/src/lib/primitives/geometry/Arc2d.ts +1 -1
  179. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +13 -0
  180. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
  181. package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
  182. package/src/lib/primitives/geometry/Edge2d.ts +1 -1
  183. package/src/lib/primitives/geometry/Group2d.test.ts +26 -5
  184. package/src/lib/primitives/geometry/Group2d.ts +18 -7
  185. package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
  186. package/src/lib/primitives/geometry/Point2d.ts +1 -1
  187. package/src/lib/test/preserveFocus.test.tsx +81 -0
  188. package/src/lib/utils/debug-flags.ts +0 -6
  189. package/src/lib/utils/deepLinks.test.ts +15 -0
  190. package/src/lib/utils/deepLinks.ts +8 -3
  191. package/src/lib/utils/sync/LocalIndexedDb.test.ts +95 -0
  192. package/src/lib/utils/sync/LocalIndexedDb.ts +31 -27
  193. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +257 -1
  194. package/src/lib/utils/sync/TLLocalSyncClient.ts +112 -54
  195. package/src/version.ts +3 -3
  196. package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
  197. package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
  198. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
  199. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
  200. package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
@@ -75,6 +75,7 @@ import {
75
75
  import {
76
76
  DEFAULT_ANIMATION_OPTIONS,
77
77
  DEFAULT_CAMERA_OPTIONS,
78
+ FRAME_MS_60HZ,
78
79
  INTERNAL_POINTER_IDS,
79
80
  LEFT_MOUSE_BUTTON,
80
81
  MIDDLE_MOUSE_BUTTON,
@@ -135,6 +136,10 @@ import { UserPreferencesManager } from "./managers/UserPreferencesManager/UserPr
135
136
  import { OverlayManager } from "./overlays/OverlayManager.mjs";
136
137
  import { RootState } from "./tools/RootState.mjs";
137
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
+ };
138
143
  class Editor extends EventEmitter {
139
144
  id = uniqueId();
140
145
  constructor({
@@ -461,7 +466,7 @@ class Editor extends EventEmitter {
461
466
  }
462
467
  }
463
468
  if (deleteBindingIds.length) {
464
- this.deleteBindings(deleteBindingIds);
469
+ this.store.remove(deleteBindingIds);
465
470
  }
466
471
  }
467
472
  },
@@ -1421,6 +1426,7 @@ class Editor extends EventEmitter {
1421
1426
  * @public
1422
1427
  **/
1423
1428
  updateDocumentSettings(settings) {
1429
+ if (this.getIsReadonly()) return this;
1424
1430
  this.run(
1425
1431
  () => {
1426
1432
  this.store.put([{ ...this.getDocumentSettings(), ...settings }]);
@@ -1580,7 +1586,7 @@ class Editor extends EventEmitter {
1580
1586
  * @public
1581
1587
  */
1582
1588
  select(...shapes) {
1583
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
1589
+ const ids = toShapeIds(shapes);
1584
1590
  this.setSelectedShapes(ids);
1585
1591
  return this;
1586
1592
  }
@@ -1595,7 +1601,7 @@ class Editor extends EventEmitter {
1595
1601
  * @public
1596
1602
  */
1597
1603
  deselect(...shapes) {
1598
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
1604
+ const ids = toShapeIds(shapes);
1599
1605
  const selectedShapeIds = this.getSelectedShapeIds();
1600
1606
  if (selectedShapeIds.length > 0 && ids.length > 0) {
1601
1607
  this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id)));
@@ -1633,7 +1639,7 @@ class Editor extends EventEmitter {
1633
1639
  }
1634
1640
  const ids = this.getSortedChildIdsForParent(parentToSelectWithinId);
1635
1641
  if (ids.length <= 0) return this;
1636
- this.setSelectedShapes(this._getUnlockedShapeIds(ids));
1642
+ this.setSelectedShapes(ids.filter((id) => !this.getShape(id)?.isLocked));
1637
1643
  return this;
1638
1644
  }
1639
1645
  /**
@@ -1650,14 +1656,18 @@ class Editor extends EventEmitter {
1650
1656
  const selectedShapeIds = this.getSelectedShapeIds();
1651
1657
  const firstParentId = selectedShapeIds[0] ? this.getShape(selectedShapeIds[0])?.parentId : null;
1652
1658
  const isSelectedWithinContainer = firstParentId && selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) && !isPageId(firstParentId);
1653
- const filteredShapes = isSelectedWithinContainer ? this.getCurrentPageShapes().filter((shape2) => shape2.parentId === firstParentId) : this.getCurrentPageShapes().filter((shape2) => isPageId(shape2.parentId));
1654
- 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);
1655
1663
  const currentShapeId = selectedShapeIds.length === 1 ? selectedShapeIds[0] : readingOrderShapes.find((shape2) => selectedShapeIds.includes(shape2.id))?.id;
1656
1664
  let adjacentShapeId;
1657
1665
  if (direction === "next" || direction === "prev") {
1658
1666
  const shapeIds = readingOrderShapes.map((shape2) => shape2.id);
1667
+ if (shapeIds.length === 0) return;
1659
1668
  const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1;
1660
- const adjacentIndex = (currentIndex + (direction === "next" ? 1 : -1) + shapeIds.length) % shapeIds.length;
1669
+ const startIndex = currentIndex === -1 && direction === "prev" ? 0 : currentIndex;
1670
+ const adjacentIndex = (startIndex + (direction === "next" ? 1 : -1) + shapeIds.length) % shapeIds.length;
1661
1671
  adjacentShapeId = shapeIds[adjacentIndex];
1662
1672
  } else {
1663
1673
  if (!currentShapeId) return;
@@ -1756,13 +1766,14 @@ class Editor extends EventEmitter {
1756
1766
  });
1757
1767
  if (shapesInDirection.length === 0) return currentShapeId;
1758
1768
  const lowestScoringShape = minBy(shapesInDirection, ({ center }) => {
1759
- const distance = Vec.Dist2(currentCenter, center);
1769
+ const distance = Vec.Dist(currentCenter, center);
1760
1770
  const dirProp = ["left", "right"].includes(direction) ? "x" : "y";
1761
1771
  const directionalDistance = Math.abs(center[dirProp] - currentCenter[dirProp]);
1762
1772
  const offProp = ["left", "right"].includes(direction) ? "y" : "x";
1763
1773
  const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp]);
1764
- const angle = Math.abs(Vec.Angle(currentCenter, center) * (180 / Math.PI));
1765
- 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);
1766
1777
  return distance * 1 + // Base distance
1767
1778
  offAxisDeviation * 2 + // Heavy penalty for off-axis deviation
1768
1779
  (distance - directionalDistance) * 1.5 + // Penalty for diagonal distance
@@ -2100,7 +2111,7 @@ class Editor extends EventEmitter {
2100
2111
  * @public
2101
2112
  */
2102
2113
  setHintingShapes(shapes) {
2103
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
2114
+ const ids = toShapeIds(shapes);
2104
2115
  this.run(
2105
2116
  () => {
2106
2117
  this._updateCurrentPageState({ hintingShapeIds: dedupe(ids) });
@@ -2130,7 +2141,7 @@ class Editor extends EventEmitter {
2130
2141
  * @public
2131
2142
  */
2132
2143
  setErasingShapes(shapes) {
2133
- const ids = typeof shapes[0] === "string" ? shapes.slice() : shapes.map((shape) => shape.id);
2144
+ const ids = toShapeIds(shapes).slice();
2134
2145
  ids.sort();
2135
2146
  const erasingShapeIds = this.getErasingShapeIds();
2136
2147
  this.run(
@@ -2537,14 +2548,16 @@ class Editor extends EventEmitter {
2537
2548
  setCamera(point, opts) {
2538
2549
  const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture();
2539
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
+ }
2540
2557
  this.stopCameraAnimation();
2541
2558
  if (this.getInstanceState().followingUserId) {
2542
2559
  this.stopFollowingUser();
2543
2560
  }
2544
- const _point = Vec.Cast(point);
2545
- if (!Number.isFinite(_point.x)) _point.x = 0;
2546
- if (!Number.isFinite(_point.y)) _point.y = 0;
2547
- if (_point.z === void 0 || !Number.isFinite(_point.z)) point.z = this.getZoomLevel();
2548
2561
  const camera = this.getConstrainedCamera(_point, opts);
2549
2562
  if (opts?.animation) {
2550
2563
  const { width, height } = this.getViewportScreenBounds();
@@ -2829,11 +2842,14 @@ class Editor extends EventEmitter {
2829
2842
  _animateViewport(ms) {
2830
2843
  if (!this._viewportAnimation) return;
2831
2844
  this._viewportAnimation.elapsed += ms;
2832
- const { elapsed, easing, duration, start, end } = this._viewportAnimation;
2845
+ const { elapsed, easing, duration, start, end, opts } = this._viewportAnimation;
2833
2846
  if (elapsed > duration) {
2834
2847
  this.off("tick", this._animateViewport);
2835
2848
  this._viewportAnimation = null;
2836
- 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
+ );
2837
2853
  return;
2838
2854
  }
2839
2855
  const remaining = duration - elapsed;
@@ -2871,7 +2887,8 @@ class Editor extends EventEmitter {
2871
2887
  duration: duration / animationSpeed,
2872
2888
  easing,
2873
2889
  start: viewportPageBounds.clone(),
2874
- end: targetViewportPage.clone()
2890
+ end: targetViewportPage.clone(),
2891
+ opts: rest
2875
2892
  };
2876
2893
  this.once("stop-camera-animation", () => {
2877
2894
  this.off("tick", this._animateViewport);
@@ -2923,7 +2940,7 @@ class Editor extends EventEmitter {
2923
2940
  newCx += center.x / newCz - center.x / cz;
2924
2941
  newCy += center.y / newCz - center.y / cz;
2925
2942
  }
2926
- currentSpeed *= 1 - friction;
2943
+ currentSpeed *= (1 - friction) ** (elapsed / FRAME_MS_60HZ);
2927
2944
  if (currentSpeed < speedThreshold) {
2928
2945
  cancel();
2929
2946
  } else {
@@ -2957,6 +2974,7 @@ class Editor extends EventEmitter {
2957
2974
  }
2958
2975
  const isOnSamePage = presence.currentPageId === this.getCurrentPageId();
2959
2976
  if (!isOnSamePage) {
2977
+ this.markHistoryStoppingPoint("change-page");
2960
2978
  this.setCurrentPage(presence.currentPageId);
2961
2979
  }
2962
2980
  if (opts && opts.animation && !isOnSamePage) {
@@ -3788,7 +3806,9 @@ class Editor extends EventEmitter {
3788
3806
  if (ids.length <= 0) return this;
3789
3807
  this.run(
3790
3808
  () => {
3791
- 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
+ );
3792
3812
  this.store.remove(ids);
3793
3813
  },
3794
3814
  { history: "ignore" }
@@ -4170,7 +4190,7 @@ class Editor extends EventEmitter {
4170
4190
  if (shapes.length === 0) {
4171
4191
  return;
4172
4192
  }
4173
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
4193
+ const ids = toShapeIds(shapes);
4174
4194
  const freshShapes = compact(ids.map((id) => this.getShape(id)));
4175
4195
  if (freshShapes.length === 1) {
4176
4196
  const parentId = freshShapes[0].parentId;
@@ -4267,12 +4287,14 @@ class Editor extends EventEmitter {
4267
4287
  */
4268
4288
  getSelectedShapeAtPoint(point) {
4269
4289
  const selectedShapeIds = this.getSelectedShapeIds();
4270
- 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();
4271
4293
  const sortedShapes = this.getCurrentPageShapesSorted();
4272
4294
  for (let i = sortedShapes.length - 1; i >= 0; i--) {
4273
4295
  const shape = sortedShapes[i];
4274
4296
  if (shape.type === "group") continue;
4275
- if (!selectedShapeIds.includes(shape.id)) continue;
4297
+ if (!selectedShapeIdSet.has(shape.id)) continue;
4276
4298
  if (this.getShapeGeometry(shape).hitTestPoint(
4277
4299
  this.getPointInShapeSpace(shape, point),
4278
4300
  margin,
@@ -4370,7 +4392,6 @@ class Editor extends EventEmitter {
4370
4392
  }
4371
4393
  return inMarginClosestToEdgeHit || shape;
4372
4394
  } else {
4373
- if (this.getShapePageBounds(shape).contains(viewportPageBounds)) continue;
4374
4395
  if (hitInside ? (
4375
4396
  // On hitInside, the distance will be negative for hits inside
4376
4397
  // If the distance is positive, check against the outer margin
@@ -4387,6 +4408,7 @@ class Editor extends EventEmitter {
4387
4408
  inMarginClosestToEdgeHit = shape;
4388
4409
  }
4389
4410
  } else if (!inMarginClosestToEdgeHit) {
4411
+ if (this.getShapePageBounds(shape).contains(viewportPageBounds)) continue;
4390
4412
  const { area } = geometry;
4391
4413
  if (area < inHollowSmallestArea) {
4392
4414
  inHollowSmallestArea = area;
@@ -4397,6 +4419,9 @@ class Editor extends EventEmitter {
4397
4419
  }
4398
4420
  } else {
4399
4421
  if (distance < this.getHitTestMargin()) {
4422
+ if (inMarginClosestToEdgeHit && inMarginClosestToEdgeDistance <= distance) {
4423
+ return inMarginClosestToEdgeHit;
4424
+ }
4400
4425
  return shape;
4401
4426
  }
4402
4427
  }
@@ -4673,7 +4698,7 @@ class Editor extends EventEmitter {
4673
4698
  * @public
4674
4699
  */
4675
4700
  reparentShapes(shapes, parentId, insertIndex) {
4676
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
4701
+ const ids = toShapeIds(shapes);
4677
4702
  if (ids.length === 0) return this;
4678
4703
  const changes = [];
4679
4704
  const parentTransform = isPageId(parentId) ? Mat.Identity() : this.getShapePageTransform(parentId);
@@ -4902,6 +4927,7 @@ class Editor extends EventEmitter {
4902
4927
  * binding, but the `type`, `toId`, and `fromId` must all be provided.
4903
4928
  */
4904
4929
  createBindings(partials) {
4930
+ if (this.getIsReadonly()) return this;
4905
4931
  const bindings = [];
4906
4932
  for (const partial of partials) {
4907
4933
  const fromShape = this.getShape(partial.fromId);
@@ -4936,6 +4962,7 @@ class Editor extends EventEmitter {
4936
4962
  * binding is skipped. The changes from the partial are merged into the existing record.
4937
4963
  */
4938
4964
  updateBindings(partials) {
4965
+ if (this.getIsReadonly()) return this;
4939
4966
  const updated = [];
4940
4967
  for (const partial of partials) {
4941
4968
  if (!partial) continue;
@@ -4964,6 +4991,16 @@ class Editor extends EventEmitter {
4964
4991
  * Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
4965
4992
  */
4966
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 } = {}) {
4967
5004
  const ids = bindings.map((binding) => typeof binding === "string" ? binding : binding.id);
4968
5005
  if (isolateShapes) {
4969
5006
  this.store.atomic(() => {
@@ -5022,7 +5059,7 @@ class Editor extends EventEmitter {
5022
5059
  * @param opts - The options for the rotation.
5023
5060
  */
5024
5061
  rotateShapesBy(shapes, delta, opts) {
5025
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5062
+ const ids = toShapeIds(shapes);
5026
5063
  if (ids.length <= 0) return this;
5027
5064
  const snapshot = getRotationSnapshot({ editor: this, ids });
5028
5065
  if (!snapshot) return this;
@@ -5059,6 +5096,14 @@ class Editor extends EventEmitter {
5059
5096
  }
5060
5097
  return workingShape;
5061
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
+ }
5062
5107
  /**
5063
5108
  * Move shapes by a delta.
5064
5109
  *
@@ -5071,14 +5116,12 @@ class Editor extends EventEmitter {
5071
5116
  * @param offset - The offset to apply to the shapes.
5072
5117
  */
5073
5118
  nudgeShapes(shapes, offset) {
5074
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5119
+ const ids = toShapeIds(shapes);
5075
5120
  if (ids.length <= 0) return this;
5076
5121
  const changes = [];
5077
5122
  for (const id of ids) {
5078
5123
  const shape = this.getShape(id);
5079
- const localDelta = Vec.From(offset);
5080
- localDelta.rot(-this.getShapeParentTransform(shape).rotation());
5081
- changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)));
5124
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, offset));
5082
5125
  }
5083
5126
  this.updateShapes(changes);
5084
5127
  return this;
@@ -5099,7 +5142,7 @@ class Editor extends EventEmitter {
5099
5142
  */
5100
5143
  duplicateShapes(shapes, offset) {
5101
5144
  this.run(() => {
5102
- const _ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5145
+ const _ids = toShapeIds(shapes);
5103
5146
  const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids);
5104
5147
  if (ids.length <= 0) return this;
5105
5148
  const initialIds = new Set(ids);
@@ -5208,7 +5251,7 @@ class Editor extends EventEmitter {
5208
5251
  * @public
5209
5252
  */
5210
5253
  moveShapesToPage(shapes, pageId) {
5211
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5254
+ const ids = toShapeIds(shapes);
5212
5255
  if (ids.length === 0) return this;
5213
5256
  if (this.getIsReadonly()) return this;
5214
5257
  const currentPageId = this.getCurrentPageId();
@@ -5244,7 +5287,7 @@ class Editor extends EventEmitter {
5244
5287
  * @public
5245
5288
  */
5246
5289
  toggleLock(shapes) {
5247
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5290
+ const ids = toShapeIds(shapes);
5248
5291
  if (this.getIsReadonly() || ids.length === 0) return this;
5249
5292
  let allLocked = true;
5250
5293
  const shapesToToggle = [];
@@ -5283,7 +5326,7 @@ class Editor extends EventEmitter {
5283
5326
  * @public
5284
5327
  */
5285
5328
  sendToBack(shapes) {
5286
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5329
+ const ids = toShapeIds(shapes);
5287
5330
  const changes = getReorderingShapesChanges(this, "toBack", ids, {
5288
5331
  considerAllShapes: true
5289
5332
  });
@@ -5313,7 +5356,7 @@ class Editor extends EventEmitter {
5313
5356
  * @public
5314
5357
  */
5315
5358
  sendBackward(shapes, opts = {}) {
5316
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5359
+ const ids = toShapeIds(shapes);
5317
5360
  const changes = getReorderingShapesChanges(this, "backward", ids, opts);
5318
5361
  if (changes) this.updateShapes(changes);
5319
5362
  return this;
@@ -5341,7 +5384,7 @@ class Editor extends EventEmitter {
5341
5384
  * @public
5342
5385
  */
5343
5386
  bringForward(shapes, opts = {}) {
5344
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5387
+ const ids = toShapeIds(shapes);
5345
5388
  const changes = getReorderingShapesChanges(this, "forward", ids, opts);
5346
5389
  if (changes) this.updateShapes(changes);
5347
5390
  return this;
@@ -5360,7 +5403,7 @@ class Editor extends EventEmitter {
5360
5403
  * @public
5361
5404
  */
5362
5405
  bringToFront(shapes) {
5363
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5406
+ const ids = toShapeIds(shapes);
5364
5407
  const changes = getReorderingShapesChanges(this, "toFront", ids);
5365
5408
  if (changes) this.updateShapes(changes);
5366
5409
  return this;
@@ -5372,7 +5415,7 @@ class Editor extends EventEmitter {
5372
5415
  * @internal
5373
5416
  */
5374
5417
  getShapeClusters(shapes, type, opts) {
5375
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5418
+ const ids = toShapeIds(shapes);
5376
5419
  let freshShapes = compact(ids.map((id) => this.getShape(id)));
5377
5420
  if (opts?.filterAxisAligned) {
5378
5421
  freshShapes = freshShapes.filter((s) => {
@@ -5397,7 +5440,7 @@ class Editor extends EventEmitter {
5397
5440
  const shapesMovingTogether = [shape];
5398
5441
  const boundsOfShapesMovingTogether = [shapePageBounds];
5399
5442
  this.collectShapesViaArrowBindings({
5400
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5443
+ bindings: this.getBindingsInvolvingShape(shape.id, "arrow"),
5401
5444
  initialShapes: freshShapes,
5402
5445
  resultShapes: shapesMovingTogether,
5403
5446
  resultBounds: boundsOfShapesMovingTogether,
@@ -5453,7 +5496,7 @@ class Editor extends EventEmitter {
5453
5496
  */
5454
5497
  flipShapes(shapes, operation) {
5455
5498
  if (this.getIsReadonly()) return this;
5456
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5499
+ const ids = toShapeIds(shapes);
5457
5500
  const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)));
5458
5501
  for (const shape of shapesToFlipFirstPass) {
5459
5502
  if (this.isShapeOfType(shape, "group")) {
@@ -5527,21 +5570,7 @@ class Editor extends EventEmitter {
5527
5570
  const { clusters: shapeClustersToStack } = this.getShapeClusters(shapes, "stack");
5528
5571
  const len = shapeClustersToStack.length;
5529
5572
  if (_gap === 0 && len < 3 || len < 2) return this;
5530
- let val;
5531
- let min;
5532
- let max;
5533
- let dim;
5534
- if (operation === "horizontal") {
5535
- val = "x";
5536
- min = "minX";
5537
- max = "maxX";
5538
- dim = "width";
5539
- } else {
5540
- val = "y";
5541
- min = "minY";
5542
- max = "maxY";
5543
- dim = "height";
5544
- }
5573
+ const { val, min, max, dim } = AXIS[operation];
5545
5574
  let shapeGap = 0;
5546
5575
  shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min]);
5547
5576
  if (_gap === 0) {
@@ -5580,13 +5609,7 @@ class Editor extends EventEmitter {
5580
5609
  const delta = new Vec();
5581
5610
  delta[val] = v + shapeGap - pageBounds[val];
5582
5611
  for (const shape of shapes2) {
5583
- const shapeDelta = delta.clone();
5584
- const parent = this.getShapeParent(shape);
5585
- if (parent) {
5586
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5587
- }
5588
- shapeDelta.add(shape);
5589
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5612
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5590
5613
  }
5591
5614
  v += pageBounds[dim] + shapeGap;
5592
5615
  }
@@ -5666,13 +5689,7 @@ class Editor extends EventEmitter {
5666
5689
  for (const { shapes: shapes2, pageBounds, nextPageBounds } of shapeClustersToPack) {
5667
5690
  const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta);
5668
5691
  for (const shape of shapes2) {
5669
- const shapeDelta = delta.clone();
5670
- const parent = this.getShapeParent(shape);
5671
- if (parent) {
5672
- shapeDelta.rot(-this.getShapeParentTransform(shape).rotation());
5673
- }
5674
- shapeDelta.add(shape);
5675
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5692
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5676
5693
  }
5677
5694
  }
5678
5695
  if (changes.length) {
@@ -5732,13 +5749,7 @@ class Editor extends EventEmitter {
5732
5749
  }
5733
5750
  }
5734
5751
  for (const shape of shapes2) {
5735
- const shapeDelta = delta.clone();
5736
- const parent = this.getShapeParent(shape);
5737
- if (parent) {
5738
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5739
- }
5740
- shapeDelta.add(shape);
5741
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5752
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5742
5753
  }
5743
5754
  });
5744
5755
  this.updateShapes(changes);
@@ -5762,27 +5773,13 @@ class Editor extends EventEmitter {
5762
5773
  if (this.getIsReadonly()) return this;
5763
5774
  const { clusters: shapeClustersToDistribute } = this.getShapeClusters(shapes, "distribute");
5764
5775
  if (shapeClustersToDistribute.length < 3) return this;
5765
- let val;
5766
- let min;
5767
- let max;
5768
- let dim;
5769
- if (operation === "horizontal") {
5770
- val = "x";
5771
- min = "minX";
5772
- max = "maxX";
5773
- dim = "width";
5774
- } else {
5775
- val = "y";
5776
- min = "minY";
5777
- max = "maxY";
5778
- dim = "height";
5779
- }
5776
+ const { val, min, max, dim } = AXIS[operation];
5780
5777
  const changes = [];
5781
5778
  const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0];
5782
5779
  const last2 = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0];
5783
5780
  if (first === last2) {
5784
5781
  const excludedShapeIds = new Set(first.shapes.map((s) => s.id));
5785
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5782
+ const ids = toShapeIds(shapes);
5786
5783
  return this.distributeShapes(
5787
5784
  ids.filter((id) => !excludedShapeIds.has(id)),
5788
5785
  operation
@@ -5806,13 +5803,7 @@ class Editor extends EventEmitter {
5806
5803
  delta[val] = last2.pageBounds[max] - pageBounds[max] - 1;
5807
5804
  }
5808
5805
  for (const shape of shapes2) {
5809
- const shapeDelta = delta.clone();
5810
- const parent = this.getShapeParent(shape);
5811
- if (parent) {
5812
- shapeDelta.rot(-this.getShapePageTransform(parent).rotation());
5813
- }
5814
- shapeDelta.add(shape);
5815
- changes.push(this.getChangesToTranslateShape(shape, shapeDelta));
5806
+ changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta));
5816
5807
  }
5817
5808
  v += pageBounds[dim] + gap;
5818
5809
  }
@@ -5842,32 +5833,17 @@ class Editor extends EventEmitter {
5842
5833
  );
5843
5834
  if (shapeClustersToStretch.length < 2) return this;
5844
5835
  const commonBounds = Box.Common(allBounds);
5845
- let val;
5846
- let min;
5847
- let dim;
5848
- if (operation === "horizontal") {
5849
- val = "x";
5850
- min = "minX";
5851
- dim = "width";
5852
- } else {
5853
- val = "y";
5854
- min = "minY";
5855
- dim = "height";
5856
- }
5836
+ const { val, min, dim } = AXIS[operation];
5857
5837
  this.run(() => {
5858
5838
  shapeClustersToStretch.forEach(({ shapes: shapes2, pageBounds }) => {
5859
- const localOffset = new Vec();
5860
- localOffset[val] = commonBounds[min] - pageBounds[min];
5839
+ const pageOffset = new Vec();
5840
+ pageOffset[val] = commonBounds[min] - pageBounds[min];
5861
5841
  const scaleOrigin = pageBounds.center.clone();
5862
5842
  scaleOrigin[val] = commonBounds[min];
5863
5843
  const scale = new Vec(1, 1);
5864
5844
  scale[val] = commonBounds[dim] / pageBounds[dim];
5865
5845
  for (const shape of shapes2) {
5866
- const shapeLocalOffset = localOffset.clone();
5867
- shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation());
5868
- shapeLocalOffset.add(shape);
5869
- const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5870
- this.updateShape(changes);
5846
+ this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset));
5871
5847
  this.resizeShape(shape.id, scale, {
5872
5848
  initialBounds: this.getShapeGeometry(shape).bounds,
5873
5849
  scaleOrigin,
@@ -5910,7 +5886,7 @@ class Editor extends EventEmitter {
5910
5886
  const scaleY = targetBounds.height / commonBounds.height;
5911
5887
  const scale = new Vec(scaleX, scaleY);
5912
5888
  shapeClusters.forEach(({ shapes: shapes2, pageBounds }) => {
5913
- const localOffset = new Vec(
5889
+ const pageOffset = new Vec(
5914
5890
  targetBounds.minX - commonBounds.minX + (pageBounds.minX - commonBounds.minX) * (scaleX - 1),
5915
5891
  targetBounds.minY - commonBounds.minY + (pageBounds.minY - commonBounds.minY) * (scaleY - 1)
5916
5892
  );
@@ -5919,11 +5895,7 @@ class Editor extends EventEmitter {
5919
5895
  targetBounds.minY + (pageBounds.minY - commonBounds.minY) * scaleY
5920
5896
  );
5921
5897
  for (const shape of shapes2) {
5922
- const shapeLocalOffset = localOffset.clone();
5923
- shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation());
5924
- shapeLocalOffset.add(shape);
5925
- const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5926
- this.updateShape(changes);
5898
+ this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset));
5927
5899
  this.resizeShape(shape.id, scale, {
5928
5900
  initialBounds: this.getShapeGeometry(shape).bounds,
5929
5901
  scaleOrigin,
@@ -6342,12 +6314,17 @@ class Editor extends EventEmitter {
6342
6314
  let remaining = duration;
6343
6315
  let t;
6344
6316
  const animations = [];
6317
+ const ignoreShapeLock = this._shouldIgnoreShapeLock;
6345
6318
  let partial, result;
6346
6319
  for (let i = 0, n = partials.length; i < n; i++) {
6347
6320
  partial = partials[i];
6348
6321
  if (!partial) continue;
6349
6322
  const shape = this.getShape(partial.id);
6350
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
+ }
6351
6328
  result = {
6352
6329
  start: structuredClone(shape),
6353
6330
  end: applyPartialToRecordWithProps(structuredClone(shape), partial)
@@ -6363,7 +6340,7 @@ class Editor extends EventEmitter {
6363
6340
  (p) => p && animatingShapes2.get(p.id) === animationId
6364
6341
  );
6365
6342
  if (partialsToUpdate.length) {
6366
- this.updateShapes(partialsToUpdate);
6343
+ this.run(() => this.updateShapes(partialsToUpdate), { ignoreShapeLock });
6367
6344
  }
6368
6345
  this.off("tick", handleTick);
6369
6346
  return;
@@ -6396,13 +6373,13 @@ class Editor extends EventEmitter {
6396
6373
  throw Error("Editor.groupShapes: must provide an array of shapes or shape ids");
6397
6374
  }
6398
6375
  if (this.getIsReadonly()) return this;
6399
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6400
- if (ids.length <= 1) return this;
6376
+ const ids = toShapeIds(shapes);
6401
6377
  const shapesToGroup = compact(
6402
6378
  (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
6403
6379
  (id) => this.getShape(id)
6404
6380
  )
6405
6381
  );
6382
+ if (shapesToGroup.length <= 1) return this;
6406
6383
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id);
6407
6384
  const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)));
6408
6385
  const pageBounds = Box.Common(childBounds);
@@ -6411,8 +6388,11 @@ class Editor extends EventEmitter {
6411
6388
  }
6412
6389
  const { x, y } = pageBounds.point;
6413
6390
  const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId();
6414
- if (this.getCurrentToolId() !== "select") return this;
6415
- 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")) {
6416
6396
  this.cancel();
6417
6397
  }
6418
6398
  const shapesWithRootParent = shapesToGroup.filter((shape) => shape.parentId === parentId).sort(sortByIndex);
@@ -6440,15 +6420,14 @@ class Editor extends EventEmitter {
6440
6420
  ungroupShapes(shapes, opts = {}) {
6441
6421
  if (this.getIsReadonly()) return this;
6442
6422
  const { select = true } = opts;
6443
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6423
+ const ids = toShapeIds(shapes);
6444
6424
  const shapesToUngroup = compact(
6445
6425
  (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map(
6446
6426
  (id) => this.getShape(id)
6447
6427
  )
6448
6428
  );
6449
6429
  if (shapesToUngroup.length === 0) return this;
6450
- if (this.getCurrentToolId() !== "select") return this;
6451
- if (!this.isIn("select.idle")) {
6430
+ if (this.isIn("select") && !this.isIn("select.idle")) {
6452
6431
  this.cancel();
6453
6432
  }
6454
6433
  const idsToSelect = /* @__PURE__ */ new Set();
@@ -6462,16 +6441,16 @@ class Editor extends EventEmitter {
6462
6441
  });
6463
6442
  if (groups.length === 0) return this;
6464
6443
  this.run(() => {
6465
- let group;
6466
6444
  for (let i = 0, n = groups.length; i < n; i++) {
6467
- group = groups[i];
6445
+ const group = this.getShape(groups[i].id);
6446
+ if (!group) continue;
6468
6447
  const childIds = this.getSortedChildIdsForParent(group.id);
6469
6448
  for (let j = 0, n2 = childIds.length; j < n2; j++) {
6470
6449
  idsToSelect.add(childIds[j]);
6471
6450
  }
6472
6451
  this.reparentShapes(childIds, group.parentId, group.index);
6473
6452
  }
6474
- this.deleteShapes(groups.map((group2) => group2.id));
6453
+ this.deleteShapes(groups.map((group) => group.id));
6475
6454
  if (select) {
6476
6455
  this.select(...idsToSelect);
6477
6456
  }
@@ -6552,14 +6531,14 @@ class Editor extends EventEmitter {
6552
6531
  }
6553
6532
  /** @internal */
6554
6533
  _getUnlockedShapeIds(ids) {
6555
- return ids.filter((id) => !this.getShape(id)?.isLocked);
6534
+ return ids.filter((id) => !this.isShapeOrAncestorLocked(id));
6556
6535
  }
6557
6536
  deleteShapes(_ids) {
6558
6537
  if (this.getIsReadonly()) return this;
6559
6538
  if (!Array.isArray(_ids)) {
6560
6539
  throw Error("Editor.deleteShapes: must provide an array of shapes or shapeIds");
6561
6540
  }
6562
- const shapeIds = typeof _ids[0] === "string" ? _ids : _ids.map((s) => s.id);
6541
+ const shapeIds = toShapeIds(_ids);
6563
6542
  const shapeIdsToDelete = this._shouldIgnoreShapeLock ? shapeIds : this._getUnlockedShapeIds(shapeIds);
6564
6543
  if (shapeIdsToDelete.length === 0) return this;
6565
6544
  const allShapeIdsToDelete = new Set(shapeIdsToDelete);
@@ -6904,7 +6883,7 @@ class Editor extends EventEmitter {
6904
6883
  * @public
6905
6884
  */
6906
6885
  getContentFromCurrentPage(shapes) {
6907
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
6886
+ const ids = toShapeIds(shapes);
6908
6887
  if (!ids) return;
6909
6888
  if (ids.length === 0) return;
6910
6889
  const shapeIds = this.getShapeAndDescendantIds(ids);
@@ -6972,21 +6951,29 @@ class Editor extends EventEmitter {
6972
6951
  await Promise.allSettled(
6973
6952
  content.assets.map(async (asset) => {
6974
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")) {
6975
- const assetWithDataUrl = structuredClone(asset);
6976
- const objectUrl = await this.store.props.assets.resolve(asset, {
6977
- screenScale: 1,
6978
- steppedScreenScale: 1,
6979
- dpr: 1,
6980
- networkEffectiveType: null,
6981
- shouldResolveToOriginal: true
6982
- });
6983
- assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(
6984
- await fetch(objectUrl).then((r) => r.blob())
6985
- );
6986
- assets.push(assetWithDataUrl);
6987
- } else {
6988
- 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
+ }
6989
6975
  }
6976
+ assets.push(asset);
6990
6977
  })
6991
6978
  );
6992
6979
  content.assets = assets;
@@ -7280,7 +7267,7 @@ class Editor extends EventEmitter {
7280
7267
  * @public
7281
7268
  */
7282
7269
  async getSvgElement(shapes, opts = {}) {
7283
- 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);
7284
7271
  if (ids.length === 0) return void 0;
7285
7272
  await this.fonts.loadRequiredFontsForCurrentPage(this.options.maxFontsToLoadBeforeRender);
7286
7273
  return exportToSvg(this, ids, opts);
@@ -8198,6 +8185,7 @@ class Editor extends EventEmitter {
8198
8185
  inputs.buttons.add(info.button);
8199
8186
  inputs.setIsPointing(true);
8200
8187
  inputs.setIsDragging(false);
8188
+ this.stopCameraAnimation();
8201
8189
  if (!isPenMode && info.isPenDirect) {
8202
8190
  this.updateInstanceState({ isPenMode: true });
8203
8191
  this.interrupt();
@@ -8334,8 +8322,8 @@ class Editor extends EventEmitter {
8334
8322
  break;
8335
8323
  }
8336
8324
  case "keyboard": {
8337
- if (info.key === "ShiftRight") info.key = "ShiftLeft";
8338
- if (info.key === "AltRight") info.key = "AltLeft";
8325
+ if (info.code === "ShiftRight") info.code = "ShiftLeft";
8326
+ if (info.code === "AltRight") info.code = "AltLeft";
8339
8327
  if (info.code === "ControlRight") info.code = "ControlLeft";
8340
8328
  if (info.code === "MetaRight") info.code = "MetaLeft";
8341
8329
  switch (info.name) {
@@ -8705,6 +8693,9 @@ function pushShapeWithDescendants(editor, id, result) {
8705
8693
  pushShapeWithDescendants(editor, childIds[i], result);
8706
8694
  }
8707
8695
  }
8696
+ function toShapeIds(shapes) {
8697
+ return typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id);
8698
+ }
8708
8699
  function withIsolatedShapes(editor, shapeIds, callback) {
8709
8700
  let result;
8710
8701
  editor.run(
@@ -8727,7 +8718,7 @@ function withIsolatedShapes(editor, shapeIds, callback) {
8727
8718
  }
8728
8719
  }
8729
8720
  }
8730
- editor.deleteBindings([...bindingsToRemove], { isolateShapes: true });
8721
+ editor._deleteBindings([...bindingsToRemove], { isolateShapes: true });
8731
8722
  try {
8732
8723
  result = Result.ok(callback(bindingsWithBoth));
8733
8724
  } catch (error) {