@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.5.0-canary.078a77321dc3

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 (288) hide show
  1. package/dist-cjs/index.d.ts +119 -49
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
  9. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
  12. package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/config/createTLStore.js +1 -1
  15. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  16. package/dist-cjs/lib/constants.js +2 -0
  17. package/dist-cjs/lib/constants.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +190 -300
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  21. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  23. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  25. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
  27. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  29. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  31. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  33. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  35. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  37. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
  39. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  41. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  42. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  43. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  44. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  45. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  46. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  47. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  48. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  49. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  50. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  51. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  52. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  53. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  54. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  55. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  56. package/dist-cjs/lib/globals/editors.js +57 -0
  57. package/dist-cjs/lib/globals/editors.js.map +7 -0
  58. package/dist-cjs/lib/globals/menus.js +9 -4
  59. package/dist-cjs/lib/globals/menus.js.map +2 -2
  60. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  61. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  63. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  64. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  65. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  66. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  67. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  68. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  69. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  70. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  71. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  72. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  73. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  74. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  75. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  76. package/dist-cjs/lib/primitives/Box.js +2 -1
  77. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  78. package/dist-cjs/lib/primitives/Vec.js +4 -3
  79. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  80. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  81. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  82. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  83. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  84. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  85. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  86. package/dist-cjs/lib/primitives/geometry/Group2d.js +71 -26
  87. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  88. package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
  89. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  90. package/dist-cjs/lib/primitives/utils.js +2 -1
  91. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  92. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  93. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  94. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  95. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  96. package/dist-cjs/lib/utils/rotation.js +1 -9
  97. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  98. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  99. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  100. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  101. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  102. package/dist-cjs/version.js +3 -3
  103. package/dist-cjs/version.js.map +1 -1
  104. package/dist-esm/index.d.mts +119 -49
  105. package/dist-esm/index.mjs +9 -2
  106. package/dist-esm/index.mjs.map +2 -2
  107. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  108. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  109. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  110. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  111. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  112. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  113. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  114. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  115. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  116. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  117. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  118. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  119. package/dist-esm/lib/constants.mjs +2 -0
  120. package/dist-esm/lib/constants.mjs.map +2 -2
  121. package/dist-esm/lib/editor/Editor.mjs +199 -301
  122. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  123. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  124. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  126. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  128. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
  130. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  131. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  132. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  133. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  134. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  135. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  136. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  137. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  138. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  139. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  140. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  141. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
  142. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
  143. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  144. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  145. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  146. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  147. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  148. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  149. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  150. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  151. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  152. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  153. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  154. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  155. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  156. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  157. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  158. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  159. package/dist-esm/lib/globals/editors.mjs +37 -0
  160. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  161. package/dist-esm/lib/globals/menus.mjs +9 -4
  162. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  163. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  164. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  165. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  166. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  167. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  168. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  169. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  170. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  171. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  172. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  173. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  174. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  175. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  176. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  177. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  178. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  179. package/dist-esm/lib/primitives/Box.mjs +2 -1
  180. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  181. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  182. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  183. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
  184. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  185. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  186. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  187. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  188. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  189. package/dist-esm/lib/primitives/geometry/Group2d.mjs +75 -27
  190. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  191. package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
  192. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  193. package/dist-esm/lib/primitives/utils.mjs +2 -1
  194. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  195. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  196. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  197. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  198. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  199. package/dist-esm/lib/utils/rotation.mjs +1 -9
  200. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  201. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  202. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  203. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  204. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  205. package/dist-esm/version.mjs +3 -3
  206. package/dist-esm/version.mjs.map +1 -1
  207. package/package.json +13 -7
  208. package/src/index.ts +6 -1
  209. package/src/lib/TldrawEditor.tsx +16 -4
  210. package/src/lib/components/MenuClickCapture.tsx +19 -18
  211. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  212. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  213. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  214. package/src/lib/config/TLUserPreferences.ts +8 -4
  215. package/src/lib/config/createTLStore.ts +1 -1
  216. package/src/lib/constants.ts +8 -0
  217. package/src/lib/editor/Editor.ts +285 -392
  218. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  219. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  220. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  221. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
  222. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
  223. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  224. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  225. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  226. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  227. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  228. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  229. package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
  230. package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
  231. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  232. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  233. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  234. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  235. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  236. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  237. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  238. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  239. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  240. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  241. package/src/lib/editor/tools/StateNode.ts +12 -2
  242. package/src/lib/globals/editors.test.ts +76 -0
  243. package/src/lib/globals/editors.ts +53 -0
  244. package/src/lib/globals/menus.test.ts +33 -0
  245. package/src/lib/globals/menus.ts +11 -4
  246. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  247. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  248. package/src/lib/hooks/useLocalStore.ts +16 -8
  249. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  250. package/src/lib/hooks/usePeerIds.ts +2 -1
  251. package/src/lib/license/LicenseManager.test.ts +160 -0
  252. package/src/lib/license/LicenseManager.ts +13 -4
  253. package/src/lib/license/LicenseProvider.tsx +24 -3
  254. package/src/lib/license/useLicenseManagerState.ts +8 -6
  255. package/src/lib/primitives/Box.test.ts +9 -1
  256. package/src/lib/primitives/Box.ts +3 -1
  257. package/src/lib/primitives/Vec.test.ts +10 -0
  258. package/src/lib/primitives/Vec.ts +4 -4
  259. package/src/lib/primitives/geometry/Arc2d.test.ts +235 -3
  260. package/src/lib/primitives/geometry/Arc2d.ts +1 -1
  261. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +186 -3
  262. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
  263. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  264. package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
  265. package/src/lib/primitives/geometry/Edge2d.ts +1 -1
  266. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  267. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  268. package/src/lib/primitives/geometry/Group2d.test.ts +419 -3
  269. package/src/lib/primitives/geometry/Group2d.ts +81 -34
  270. package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
  271. package/src/lib/primitives/geometry/Point2d.ts +1 -1
  272. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  273. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  274. package/src/lib/primitives/utils.test.ts +10 -1
  275. package/src/lib/primitives/utils.ts +4 -1
  276. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  277. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  278. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  279. package/src/lib/utils/deepLinks.test.ts +15 -0
  280. package/src/lib/utils/deepLinks.ts +7 -1
  281. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  282. package/src/lib/utils/normalizeWheel.ts +6 -0
  283. package/src/lib/utils/rotation.ts +4 -12
  284. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  285. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  286. package/src/lib/utils/sync/alerts.ts +1 -1
  287. package/src/version.ts +3 -3
  288. package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
@@ -72,6 +72,7 @@ import {
72
72
  Result,
73
73
  ZERO_INDEX_KEY,
74
74
  annotateError,
75
+ areArraysShallowEqual,
75
76
  assert,
76
77
  assertExists,
77
78
  bind,
@@ -109,6 +110,7 @@ import {
109
110
  import {
110
111
  DEFAULT_ANIMATION_OPTIONS,
111
112
  DEFAULT_CAMERA_OPTIONS,
113
+ FRAME_MS_60HZ,
112
114
  INTERNAL_POINTER_IDS,
113
115
  LEFT_MOUSE_BUTTON,
114
116
  MIDDLE_MOUSE_BUTTON,
@@ -118,8 +120,10 @@ import {
118
120
  import { getOwnerWindow } from '../exports/domUtils'
119
121
  import { exportToSvg } from '../exports/exportToSvg'
120
122
  import { getSvgAsImageWithOptions, trimSvgToContent } from '../exports/getSvgAsImage'
123
+ import { registerMountedEditor, unregisterMountedEditor } from '../globals/editors'
121
124
  import { tlmenus } from '../globals/menus'
122
125
  import { tltime } from '../globals/time'
126
+ import { LicenseManager } from '../license/LicenseManager'
123
127
  import { TldrawOptions, defaultTldrawOptions } from '../options'
124
128
  import { Box, BoxLike } from '../primitives/Box'
125
129
  import { EASINGS } from '../primitives/easings'
@@ -127,7 +131,13 @@ import { Geometry2d } from '../primitives/geometry/Geometry2d'
127
131
  import { Group2d } from '../primitives/geometry/Group2d'
128
132
  import { intersectPolygonPolygon } from '../primitives/intersect'
129
133
  import { Mat, MatLike } from '../primitives/Mat'
130
- import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
134
+ import {
135
+ HALF_PI,
136
+ approximately,
137
+ areAnglesCompatible,
138
+ clamp,
139
+ pointInPolygon,
140
+ } from '../primitives/utils'
131
141
  import { Vec, VecLike } from '../primitives/Vec'
132
142
  import { areShapesContentEqual } from '../utils/areShapesContentEqual'
133
143
  import { dataUrlToFile } from '../utils/assets'
@@ -185,6 +195,7 @@ import { TLHistoryBatchOptions } from './types/history-types'
185
195
  import {
186
196
  OptionalKeys,
187
197
  RequiredKeys,
198
+ TLCameraConstraints,
188
199
  TLCameraMoveOptions,
189
200
  TLCameraOptions,
190
201
  TLGetShapeAtPointOptions,
@@ -543,6 +554,14 @@ export class Editor extends EventEmitter<TLEventMap> {
543
554
  nextPageState.editingShapeId = null
544
555
  }
545
556
 
557
+ if (
558
+ prevPageState.croppingShapeId &&
559
+ shapesNoLongerInPage.has(prevPageState.croppingShapeId)
560
+ ) {
561
+ if (!nextPageState) nextPageState = { ...prevPageState }
562
+ nextPageState.croppingShapeId = null
563
+ }
564
+
546
565
  const hintingShapeIds = prevPageState.hintingShapeIds.filter(
547
566
  (id) => !shapesNoLongerInPage.has(id)
548
567
  )
@@ -828,10 +847,11 @@ export class Editor extends EventEmitter<TLEventMap> {
828
847
 
829
848
  if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
830
849
  // ensure that descendants and ancestors are not selected at the same time
850
+ const selectedShapeIds = new Set(next.selectedShapeIds)
831
851
  const filtered = next.selectedShapeIds.filter((id) => {
832
852
  let parentId = this.getShape(id)?.parentId
833
853
  while (isShapeId(parentId)) {
834
- if (next.selectedShapeIds.includes(parentId)) {
854
+ if (selectedShapeIds.has(parentId)) {
835
855
  return false
836
856
  }
837
857
  parentId = this.getShape(parentId)?.parentId
@@ -918,10 +938,12 @@ export class Editor extends EventEmitter<TLEventMap> {
918
938
 
919
939
  this.on('mount', () => {
920
940
  this._isMounted.set(true)
941
+ registerMountedEditor(this)
921
942
  })
922
943
 
923
944
  this.on('unmount', () => {
924
945
  this._isMounted.set(false)
946
+ unregisterMountedEditor(this)
925
947
  })
926
948
 
927
949
  this.timers.requestAnimationFrame(() => {
@@ -934,7 +956,11 @@ export class Editor extends EventEmitter<TLEventMap> {
934
956
  const mode = this.store.props.collaboration.mode
935
957
  this.disposables.add(
936
958
  react('update collaboration mode', () => {
937
- this.store.put([{ ...this.getInstanceState(), isReadonly: mode.get() === 'readonly' }])
959
+ const isReadonly = mode.get() === 'readonly'
960
+ // only track `mode`, and keep the sync out of the user's undo history
961
+ unsafe__withoutCapture(() =>
962
+ this._updateInstanceState({ isReadonly }, { history: 'ignore' })
963
+ )
938
964
  })
939
965
  )
940
966
  }
@@ -973,6 +999,15 @@ export class Editor extends EventEmitter<TLEventMap> {
973
999
 
974
1000
  readonly options: TldrawOptions
975
1001
 
1002
+ /**
1003
+ * The license manager whose feature flags apply to this editor. Assigned by `<TldrawEditor />`
1004
+ * when it creates the editor, so that UI rendered outside the editor tree can resolve the
1005
+ * editor's license through the editor instance. Undefined for editors created directly.
1006
+ *
1007
+ * @internal
1008
+ */
1009
+ licenseManager?: LicenseManager
1010
+
976
1011
  readonly contextId = uniqueId()
977
1012
 
978
1013
  /**
@@ -1209,6 +1244,9 @@ export class Editor extends EventEmitter<TLEventMap> {
1209
1244
  if (this._isMounted.get()) {
1210
1245
  this.emit('unmount')
1211
1246
  }
1247
+ // The unmount listener above normally handles this; unregister again in case a listener
1248
+ // threw or was removed, so a disposed editor never lingers in `tleditors`.
1249
+ unregisterMountedEditor(this)
1212
1250
 
1213
1251
  // Stop any in-progress camera animations and following before
1214
1252
  // running disposables, so their cleanup listeners fire first
@@ -1374,7 +1412,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1374
1412
  getShapeUtil(arg: string | { type: string }) {
1375
1413
  const type = typeof arg === 'string' ? arg : arg.type
1376
1414
  const shapeUtil = getOwnProperty(this.shapeUtils, type)
1377
- assert(shapeUtil, `No shape util found for type "${type}"`)
1415
+ // hot path: avoid building the message (and the assert wrapper) on every successful call
1416
+ if (!shapeUtil) throw new Error(`No shape util found for type "${type}"`)
1378
1417
  return shapeUtil
1379
1418
  }
1380
1419
 
@@ -2265,8 +2304,11 @@ export class Editor extends EventEmitter<TLEventMap> {
2265
2304
  const shapeIds = readingOrderShapes.map((shape) => shape.id)
2266
2305
 
2267
2306
  const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1
2307
+ // With no current index, stepping from -1 makes 'prev' land one shape
2308
+ // before the last; seed it from 0 so it wraps to the last shape. See #10559.
2309
+ const startIndex = currentIndex === -1 && direction === 'prev' ? 0 : currentIndex
2268
2310
  const adjacentIndex =
2269
- (currentIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length
2311
+ (startIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length
2270
2312
  adjacentShapeId = shapeIds[adjacentIndex]
2271
2313
  } else {
2272
2314
  if (!currentShapeId) return
@@ -2451,9 +2493,9 @@ export class Editor extends EventEmitter<TLEventMap> {
2451
2493
  const selectedShapes = this.getSelectedShapes()
2452
2494
  if (!selectedShapes.length) return
2453
2495
  const selectedShape = selectedShapes[0]
2454
- const children = this.getSortedChildIdsForParent(selectedShape.id)
2455
- .map((id) => this.getShape(id))
2456
- .filter((i) => i) as TLShape[]
2496
+ const children = compact(
2497
+ this.getSortedChildIdsForParent(selectedShape.id).map((id) => this.getShape(id))
2498
+ )
2457
2499
  const sortedChildren = this._getShapesInReadingOrder(children)
2458
2500
  if (sortedChildren.length === 0) return
2459
2501
  this._selectShapesAndZoom([sortedChildren[0].id])
@@ -2554,20 +2596,14 @@ export class Editor extends EventEmitter<TLEventMap> {
2554
2596
  * @internal
2555
2597
  */
2556
2598
  getShapesSharedRotation(shapeIds: TLShapeId[]) {
2557
- let foundFirst = false // annoying but we can't use an i===0 check because we need to skip over undefineds
2558
2599
  let rotation = 0
2559
2600
  for (let i = 0, n = shapeIds.length; i < n; i++) {
2560
- const pageTransform = this.getShapePageTransform(shapeIds[i])
2561
- if (!pageTransform) continue
2562
- if (foundFirst) {
2563
- if (pageTransform.rotation() !== rotation) {
2564
- // There are at least 2 different rotations, so the common rotation is zero
2565
- return 0
2566
- }
2567
- } else {
2568
- // First rotation found
2569
- foundFirst = true
2570
- rotation = pageTransform.rotation()
2601
+ const pageRotation = this.getShapePageTransform(shapeIds[i]).rotation()
2602
+ if (i === 0) {
2603
+ rotation = pageRotation
2604
+ } else if (pageRotation !== rotation) {
2605
+ // There are at least 2 different rotations, so the common rotation is zero
2606
+ return 0
2571
2607
  }
2572
2608
  }
2573
2609
 
@@ -2599,7 +2635,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2599
2635
 
2600
2636
  if (shapeIds.length === 1) {
2601
2637
  const bounds = this.getShapeGeometry(shapeIds[0]).bounds.clone()
2602
- const pageTransform = this.getShapePageTransform(shapeIds[0])!
2638
+ const pageTransform = this.getShapePageTransform(shapeIds[0])
2603
2639
  bounds.point = pageTransform.applyToPoint(bounds.point)
2604
2640
  return bounds
2605
2641
  }
@@ -2607,11 +2643,9 @@ export class Editor extends EventEmitter<TLEventMap> {
2607
2643
  // need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box
2608
2644
  const boxFromRotatedVertices = Box.FromPoints(
2609
2645
  shapeIds
2610
- .flatMap((id) => {
2611
- const pageTransform = this.getShapePageTransform(id)
2612
- if (!pageTransform) return []
2613
- return pageTransform.applyToPoints(this.getShapeGeometry(id).bounds.corners)
2614
- })
2646
+ .flatMap((id) =>
2647
+ this.getShapePageTransform(id).applyToPoints(this.getShapeGeometry(id).bounds.corners)
2648
+ )
2615
2649
  .map((p) => p.rot(-selectionRotation))
2616
2650
  )
2617
2651
  // now position box so that it's top-left corner is in the right place
@@ -2638,9 +2672,16 @@ export class Editor extends EventEmitter<TLEventMap> {
2638
2672
  @computed getSelectionRotatedScreenBounds(): Box | undefined {
2639
2673
  const bounds = this.getSelectionRotatedPageBounds()
2640
2674
  if (!bounds) return undefined
2641
- const { x, y } = this.pageToScreen(bounds.point)
2642
- const zoom = this.getZoomLevel()
2643
- return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
2675
+ // Don't use pageToScreen here: it reads the screen bounds without capturing them, so this
2676
+ // computed would never invalidate when the container moves
2677
+ const screenBounds = this.getViewportScreenBounds()
2678
+ const { x: cx, y: cy, z: zoom } = this.getCamera()
2679
+ return new Box(
2680
+ (bounds.x + cx) * zoom + screenBounds.x,
2681
+ (bounds.y + cy) * zoom + screenBounds.y,
2682
+ bounds.width * zoom,
2683
+ bounds.height * zoom
2684
+ )
2644
2685
  }
2645
2686
 
2646
2687
  // Focus Group
@@ -2780,57 +2821,38 @@ export class Editor extends EventEmitter<TLEventMap> {
2780
2821
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2781
2822
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2782
2823
 
2783
- if (!id) {
2784
- // setting the editing shape to null
2824
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2825
+ if (id && !this.canEditShape(id)) return this
2826
+
2827
+ this.run(() => {
2828
+ // Clean up the previous editing shape. This runs outside the history-ignored batch below,
2829
+ // otherwise document changes made by onEditEnd (e.g. deleting an empty text shape) are
2830
+ // never recorded and leave a phantom undo entry whose redo resurrects the shape.
2831
+ const prevEditingShapeId = this.getEditingShapeId()
2832
+ if (prevEditingShapeId) {
2833
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2834
+ if (prevEditingShape) {
2835
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2836
+ }
2837
+ }
2838
+
2785
2839
  this.run(
2786
2840
  () => {
2787
- // Clean up the previous editing shape
2788
- const prevEditingShapeId = this.getEditingShapeId()
2789
- if (prevEditingShapeId) {
2790
- const prevEditingShape = this.getShape(prevEditingShapeId)
2791
- if (prevEditingShape) {
2792
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2793
- }
2794
- }
2795
-
2796
2841
  // Clean up the editing shape state and rich text editor
2797
2842
  this._updateCurrentPageState({ editingShapeId: null })
2798
2843
  this._currentRichTextEditor.set(null)
2799
- },
2800
- { history: 'ignore' }
2801
- )
2802
2844
 
2803
- return this
2804
- }
2845
+ if (!id) return
2805
2846
 
2806
- // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2807
- if (!this.canEditShape(id)) return this
2847
+ this.select(id)
2848
+ this._updateCurrentPageState({ editingShapeId: id })
2808
2849
 
2809
- // id was provided and the next editing shape is editable, so set the rich text editor to null
2810
- this.run(
2811
- () => {
2812
- // Clean up the previous editing shape
2813
- const prevEditingShapeId = this.getEditingShapeId()
2814
- if (prevEditingShapeId) {
2815
- const prevEditingShape = this.getShape(prevEditingShapeId)
2816
- if (prevEditingShape) {
2817
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2818
- }
2819
- }
2820
-
2821
- // Clean up the editing shape state and rich text editor
2822
- this._updateCurrentPageState({ editingShapeId: null })
2823
- this._currentRichTextEditor.set(null)
2824
-
2825
- // Set the new editing shape
2826
- this.select(id)
2827
- this._updateCurrentPageState({ editingShapeId: id })
2828
-
2829
- const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2830
- this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2831
- },
2832
- { history: 'ignore' }
2833
- )
2850
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2851
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2852
+ },
2853
+ { history: 'ignore' }
2854
+ )
2855
+ })
2834
2856
 
2835
2857
  return this
2836
2858
  }
@@ -2995,26 +3017,17 @@ export class Editor extends EventEmitter<TLEventMap> {
2995
3017
  * @public
2996
3018
  */
2997
3019
  setErasingShapes(shapes: TLShapeId[] | TLShape[]): this {
3020
+ // copy before sorting: the caller may pass a store-owned (frozen) array
2998
3021
  const ids =
2999
3022
  typeof shapes[0] === 'string'
3000
- ? (shapes as TLShapeId[])
3023
+ ? (shapes as TLShapeId[]).slice()
3001
3024
  : (shapes as TLShape[]).map((shape) => shape.id)
3002
3025
  ids.sort() // sort the incoming ids
3003
3026
  const erasingShapeIds = this.getErasingShapeIds()
3004
3027
  this.run(
3005
3028
  () => {
3006
- if (ids.length === erasingShapeIds.length) {
3007
- // if the new ids are the same length as the current ids, they might be the same.
3008
- // presuming the current ids are also sorted, check each item to see if it's the same;
3009
- // if we find any unequal, then we know the new ids are different.
3010
- for (let i = 0; i < ids.length; i++) {
3011
- if (ids[i] !== erasingShapeIds[i]) {
3012
- this._updateCurrentPageState({ erasingShapeIds: ids })
3013
- break
3014
- }
3015
- }
3016
- } else {
3017
- // if the ids are a different length, then we know they're different.
3029
+ // the current ids are also sorted, so a shallow comparison tells us whether they changed
3030
+ if (!areArraysShallowEqual(ids, erasingShapeIds)) {
3018
3031
  this._updateCurrentPageState({ erasingShapeIds: ids })
3019
3032
  }
3020
3033
  },
@@ -3051,6 +3064,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3051
3064
  if (!_shape) return false
3052
3065
  const util = this.getShapeUtil(_shape)
3053
3066
  if (!util.canCrop(_shape)) return false
3067
+ if (this.getIsReadonly()) return false
3054
3068
  if (this.isShapeOrAncestorLocked(_shape)) return false
3055
3069
  return true
3056
3070
  }
@@ -3130,11 +3144,13 @@ export class Editor extends EventEmitter<TLEventMap> {
3130
3144
  const collaborators = this.getCollaborators()
3131
3145
  let leaderPresence = null as null | TLInstancePresence
3132
3146
  while (targetUserId && !visited.includes(targetUserId)) {
3133
- leaderPresence = collaborators.find((c) => c.userId === targetUserId) ?? null
3134
- targetUserId = leaderPresence?.followingUserId ?? null
3135
- if (leaderPresence) {
3136
- visited.push(leaderPresence.userId)
3137
- }
3147
+ const nextPresence = collaborators.find((c) => c.userId === targetUserId)
3148
+ // Stop at the last resolvable presence, otherwise a leader whose own leader has left
3149
+ // (or whose presence hasn't arrived yet) can't be followed at all
3150
+ if (!nextPresence) break
3151
+ leaderPresence = nextPresence
3152
+ targetUserId = nextPresence.followingUserId ?? null
3153
+ visited.push(nextPresence.userId)
3138
3154
  }
3139
3155
  return leaderPresence
3140
3156
  }
@@ -3252,44 +3268,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3252
3268
  *
3253
3269
  * @public */
3254
3270
  getInitialZoom() {
3255
- const cameraOptions = this.getCameraOptions()
3256
- // If no camera constraints are provided, the default zoom is 100%
3257
- if (!cameraOptions.constraints) return 1
3258
-
3259
- // When defaultZoom is default, the default zoom is 100%
3260
- if (cameraOptions.constraints.initialZoom === 'default') return 1
3261
-
3262
- const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
3263
-
3264
- switch (cameraOptions.constraints.initialZoom) {
3265
- case 'fit-min': {
3266
- return Math.max(zx, zy)
3267
- }
3268
- case 'fit-max': {
3269
- return Math.min(zx, zy)
3270
- }
3271
- case 'fit-x': {
3272
- return zx
3273
- }
3274
- case 'fit-y': {
3275
- return zy
3276
- }
3277
- case 'fit-min-100': {
3278
- return Math.min(1, Math.max(zx, zy))
3279
- }
3280
- case 'fit-max-100': {
3281
- return Math.min(1, Math.min(zx, zy))
3282
- }
3283
- case 'fit-x-100': {
3284
- return Math.min(1, zx)
3285
- }
3286
- case 'fit-y-100': {
3287
- return Math.min(1, zy)
3288
- }
3289
- default: {
3290
- throw exhaustiveSwitchError(cameraOptions.constraints.initialZoom)
3291
- }
3292
- }
3271
+ return this._getFitZoom(this.getCameraOptions().constraints?.initialZoom ?? 'default')
3293
3272
  }
3294
3273
 
3295
3274
  /**
@@ -3302,16 +3281,16 @@ export class Editor extends EventEmitter<TLEventMap> {
3302
3281
  *
3303
3282
  * @public */
3304
3283
  getBaseZoom() {
3305
- const cameraOptions = this.getCameraOptions()
3306
- // If no camera constraints are provided, the default zoom is 100%
3307
- if (!cameraOptions.constraints) return 1
3284
+ return this._getFitZoom(this.getCameraOptions().constraints?.baseZoom ?? 'default')
3285
+ }
3308
3286
 
3309
- // When defaultZoom is default, the default zoom is 100%
3310
- if (cameraOptions.constraints.baseZoom === 'default') return 1
3287
+ private _getFitZoom(fit: TLCameraConstraints['initialZoom']): number {
3288
+ const cameraOptions = this.getCameraOptions()
3289
+ if (!cameraOptions.constraints || fit === 'default') return 1
3311
3290
 
3312
3291
  const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
3313
3292
 
3314
- switch (cameraOptions.constraints.baseZoom) {
3293
+ switch (fit) {
3315
3294
  case 'fit-min': {
3316
3295
  return Math.max(zx, zy)
3317
3296
  }
@@ -3337,7 +3316,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3337
3316
  return Math.min(1, zy)
3338
3317
  }
3339
3318
  default: {
3340
- throw exhaustiveSwitchError(cameraOptions.constraints.baseZoom)
3319
+ throw exhaustiveSwitchError(fit)
3341
3320
  }
3342
3321
  }
3343
3322
  }
@@ -3374,7 +3353,9 @@ export class Editor extends EventEmitter<TLEventMap> {
3374
3353
  ...this._cameraOptions.__unsafe__getWithoutCapture(),
3375
3354
  ...opts,
3376
3355
  })
3377
- if (next.zoomSteps?.length < 1) next.zoomSteps = [1]
3356
+ // `undefined < 1` is false, so an explicit `zoomSteps: undefined` would otherwise get through
3357
+ // and make every later camera move throw
3358
+ if (!next.zoomSteps || next.zoomSteps.length < 1) next.zoomSteps = [1]
3378
3359
  this._cameraOptions.set(next)
3379
3360
  this.setCamera(this.getCamera())
3380
3361
  return this
@@ -3420,8 +3401,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3420
3401
  const { constraints } = cameraOptions
3421
3402
 
3422
3403
  // Clamp padding to half the viewport size on either dimension
3423
- const py = Math.min(constraints.padding.y, vsb.w / 2)
3424
- const px = Math.min(constraints.padding.x, vsb.h / 2)
3404
+ const px = Math.min(constraints.padding.x, vsb.w / 2)
3405
+ const py = Math.min(constraints.padding.y, vsb.h / 2)
3425
3406
 
3426
3407
  // Expand the bounds by the padding
3427
3408
  const bounds = Box.From(cameraOptions.constraints.bounds)
@@ -3490,14 +3471,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3490
3471
  }
3491
3472
  case 'inside': {
3492
3473
  // When below fit zoom, constrain the camera so that the bounds stay completely within the viewport
3493
- if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w)
3474
+ if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w - bounds.x)
3494
3475
  // When above fit zoom, keep the bounds within padding distance of the viewport edge
3495
3476
  else x = clamp(x, minX + freeW, minX)
3496
3477
  break
3497
3478
  }
3498
3479
  case 'outside': {
3499
3480
  // Constrain the camera so that the bounds never leaves the viewport
3500
- x = clamp(x, px / z - bounds.w, (vsb.w - px) / z)
3481
+ x = clamp(x, px / z - bounds.w - bounds.x, (vsb.w - px) / z - bounds.x)
3501
3482
  break
3502
3483
  }
3503
3484
  case 'free': {
@@ -3522,12 +3503,12 @@ export class Editor extends EventEmitter<TLEventMap> {
3522
3503
  break
3523
3504
  }
3524
3505
  case 'inside': {
3525
- if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h)
3506
+ if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h - bounds.y)
3526
3507
  else y = clamp(y, minY + freeH, minY)
3527
3508
  break
3528
3509
  }
3529
3510
  case 'outside': {
3530
- y = clamp(y, py / z - bounds.h, (vsb.h - py) / z)
3511
+ y = clamp(y, py / z - bounds.h - bounds.y, (vsb.h - py) / z - bounds.y)
3531
3512
  break
3532
3513
  }
3533
3514
  case 'free': {
@@ -3614,6 +3595,18 @@ export class Editor extends EventEmitter<TLEventMap> {
3614
3595
  const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture()
3615
3596
  if (isLocked && !opts?.force) return this
3616
3597
 
3598
+ const _point = Vec.Cast(point)
3599
+
3600
+ // Reject non-finite values before anything else, so the call is a no-op rather than a
3601
+ // partial one. An animated move writes the camera from a 'tick' listener, and a listener
3602
+ // that throws stops TickManager scheduling the next frame, which kills every frame-driven
3603
+ // behavior for the rest of the session instead of surfacing the error to the caller.
3604
+ if (!Number.isFinite(_point.x) || !Number.isFinite(_point.y) || !Number.isFinite(_point.z)) {
3605
+ throw Error(
3606
+ `Editor.setCamera: expected finite values, got (${_point.x}, ${_point.y}, ${_point.z}).`
3607
+ )
3608
+ }
3609
+
3617
3610
  // Stop any camera animations
3618
3611
  this.stopCameraAnimation()
3619
3612
 
@@ -3622,12 +3615,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3622
3615
  this.stopFollowingUser()
3623
3616
  }
3624
3617
 
3625
- const _point = Vec.Cast(point)
3626
-
3627
- if (!Number.isFinite(_point.x)) _point.x = 0
3628
- if (!Number.isFinite(_point.y)) _point.y = 0
3629
- if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel()
3630
-
3631
3618
  const camera = this.getConstrainedCamera(_point, opts)
3632
3619
 
3633
3620
  if (opts?.animation) {
@@ -3982,9 +3969,9 @@ export class Editor extends EventEmitter<TLEventMap> {
3982
3969
  const remaining = duration - elapsed
3983
3970
  const t = easing(1 - remaining / duration)
3984
3971
 
3985
- const left = start.minX + (end.minX - start.minX) * t
3986
- const top = start.minY + (end.minY - start.minY) * t
3987
- const right = start.maxX + (end.maxX - start.maxX) * t
3972
+ const left = lerp(start.minX, end.minX, t)
3973
+ const top = lerp(start.minY, end.minY, t)
3974
+ const right = lerp(start.maxX, end.maxX, t)
3988
3975
 
3989
3976
  this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), {
3990
3977
  force: true,
@@ -4099,17 +4086,18 @@ export class Editor extends EventEmitter<TLEventMap> {
4099
4086
  let newCy = cy + dy
4100
4087
  let newCz = cz
4101
4088
 
4102
- // animate zoom if z direction is passed in
4089
+ // animate zoom if z direction is passed in. Use an exponential factor so that a single
4090
+ // long frame can't drive the zoom to zero or negative (which would give NaN coordinates)
4103
4091
  if (dirZ !== 0) {
4104
- newCz = cz * (1 + dirZ * currentSpeed * elapsed)
4092
+ newCz = cz * Math.exp(dirZ * currentSpeed * elapsed)
4105
4093
  // Adjust x/y to keep the viewport center fixed while zooming
4106
4094
  const center = this.getViewportScreenCenter()
4107
4095
  newCx += center.x / newCz - center.x / cz
4108
4096
  newCy += center.y / newCz - center.y / cz
4109
4097
  }
4110
4098
 
4111
- // Apply friction
4112
- currentSpeed *= 1 - friction
4099
+ // Apply friction per unit of elapsed time, not per tick, or a 120 Hz display decays twice as fast
4100
+ currentSpeed *= (1 - friction) ** (elapsed / FRAME_MS_60HZ)
4113
4101
  if (currentSpeed < speedThreshold) {
4114
4102
  cancel()
4115
4103
  } else {
@@ -4686,7 +4674,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4686
4674
  let nextIndex = this.options.maxShapesPerPage * 2
4687
4675
  let nextBackgroundIndex = this.options.maxShapesPerPage
4688
4676
 
4689
- const erasingShapeIds = this.getErasingShapeIds()
4677
+ const erasingShapeIds = new Set(this.getErasingShapeIds())
4690
4678
 
4691
4679
  const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
4692
4680
  const shape = this.getShape(id)
@@ -4694,7 +4682,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4694
4682
 
4695
4683
  if (this.isShapeHidden(shape)) {
4696
4684
  // process children just in case they are overriding the hidden state
4697
- const isErasing = isAncestorErasing || erasingShapeIds.includes(id)
4685
+ const isErasing = isAncestorErasing || erasingShapeIds.has(id)
4698
4686
  for (const childId of this.getSortedChildIdsForParent(id)) {
4699
4687
  addShapeById(childId, opacity, isErasing)
4700
4688
  }
@@ -4706,7 +4694,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4706
4694
  const util = this.getShapeUtil(shape)
4707
4695
 
4708
4696
  if (useEditorState) {
4709
- isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id)
4697
+ isShapeErasing = !isAncestorErasing && erasingShapeIds.has(id)
4710
4698
  if (isShapeErasing) {
4711
4699
  opacity *= 0.32
4712
4700
  }
@@ -5465,11 +5453,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5465
5453
  /** @internal */
5466
5454
  @computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
5467
5455
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
5468
- const pageTransform = this.getShapePageTransform(shape)
5469
- if (!pageTransform) return undefined
5470
-
5471
5456
  return Box.FromPoints(
5472
- pageTransform.applyToPoints(this.getShapeGeometry(shape).boundsVertices)
5457
+ this.getShapePageTransform(shape).applyToPoints(this.getShapeGeometry(shape).boundsVertices)
5473
5458
  )
5474
5459
  })
5475
5460
  }
@@ -5608,7 +5593,13 @@ export class Editor extends EventEmitter<TLEventMap> {
5608
5593
  if (pageMask) {
5609
5594
  if (pageMask.length === 0) return undefined
5610
5595
  const { corners } = pageBounds
5611
- if (corners.every((p, i) => p && Vec.Equals(p, pageMask[i]))) return pageBounds.clone()
5596
+ // the mask may have fewer than four points (e.g. a triangular clip path)
5597
+ if (
5598
+ pageMask.length === corners.length &&
5599
+ corners.every((p, i) => Vec.Equals(p, pageMask[i]))
5600
+ ) {
5601
+ return pageBounds.clone()
5602
+ }
5612
5603
  const intersection = intersectPolygonPolygon(pageMask, corners)
5613
5604
  if (!intersection) return
5614
5605
  return Box.FromPoints(intersection)
@@ -5862,6 +5853,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5862
5853
  const margin = this.options.hitTestMargin / this.getZoomLevel()
5863
5854
  const sortedShapes = this.getCurrentPageShapesSorted()
5864
5855
 
5856
+ // iterate from the top (highest z-index) to find the top-most matching shape
5865
5857
  for (let i = sortedShapes.length - 1; i >= 0; i--) {
5866
5858
  const shape = sortedShapes[i]
5867
5859
  if (shape.type === 'group') continue
@@ -5911,44 +5903,43 @@ export class Editor extends EventEmitter<TLEventMap> {
5911
5903
  const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin())
5912
5904
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5913
5905
 
5914
- const shapesToCheck = (
5915
- opts.renderingOnly
5916
- ? this.getCurrentPageRenderingShapesSorted()
5917
- : this.getCurrentPageShapesSorted()
5918
- ).filter((shape) => {
5919
- // Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
5920
- if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) return false
5906
+ const shapesToCheck = opts.renderingOnly
5907
+ ? this.getCurrentPageRenderingShapesSorted()
5908
+ : this.getCurrentPageShapesSorted()
5921
5909
 
5910
+ for (let i = shapesToCheck.length - 1; i >= 0; i--) {
5911
+ const shape = shapesToCheck[i]
5912
+ // Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
5913
+ if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
5922
5914
  if (
5923
5915
  (shape.isLocked && !hitLocked) ||
5924
5916
  this.isShapeHidden(shape) ||
5925
5917
  this.isShapeOfType(shape, 'group')
5926
- )
5927
- return false
5918
+ ) {
5919
+ continue
5920
+ }
5928
5921
  const pageMask = this.getShapeMask(shape)
5929
- if (pageMask && !pointInPolygon(point, pageMask)) return false
5930
- if (filter && !filter(shape)) return false
5931
- return true
5932
- })
5922
+ if (pageMask && !pointInPolygon(point, pageMask)) continue
5923
+ if (filter && !filter(shape)) continue
5933
5924
 
5934
- for (let i = shapesToCheck.length - 1; i >= 0; i--) {
5935
- const shape = shapesToCheck[i]
5936
5925
  const geometry = this.getShapeGeometry(shape)
5937
5926
  const isGroup = geometry instanceof Group2d
5938
5927
 
5939
5928
  const pointInShapeSpace = this.getPointInShapeSpace(shape, point)
5940
5929
 
5941
- // Check labels first
5930
+ // Check labels first. Only group geometries can carry a label child; a frame-like
5931
+ // shape util may still return a plain geometry.
5942
5932
  const shapeUtil = this.getShapeUtil(shape)
5943
5933
  const isShapeFrameLike = this.isShapeFrameLike(shape)
5944
5934
  if (
5945
- isShapeFrameLike ||
5946
- ((this.isShapeOfType(shape, 'note') ||
5947
- this.isShapeOfType(shape, 'arrow') ||
5948
- (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5949
- shapeUtil.getText(shape)?.trim())
5935
+ isGroup &&
5936
+ (isShapeFrameLike ||
5937
+ ((this.isShapeOfType(shape, 'note') ||
5938
+ this.isShapeOfType(shape, 'arrow') ||
5939
+ (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5940
+ shapeUtil.getText(shape)?.trim()))
5950
5941
  ) {
5951
- for (const childGeometry of (geometry as Group2d).children) {
5942
+ for (const childGeometry of geometry.children) {
5952
5943
  if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) {
5953
5944
  return shape
5954
5945
  }
@@ -6114,15 +6105,18 @@ export class Editor extends EventEmitter<TLEventMap> {
6114
6105
  const margin = opts.margin ?? 0
6115
6106
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
6116
6107
 
6117
- // Get all page shapes in z-index order and filter to candidates that pass isPointInShape
6118
- // Frame-like shapes are always checked because their labels can be outside their bounds
6119
- return this.getCurrentPageShapesSorted()
6120
- .filter((shape) => {
6121
- if (this.isShapeHidden(shape)) return false
6122
- if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) return false
6123
- return this.isPointInShape(shape, point, opts)
6124
- })
6125
- .reverse()
6108
+ // Get all page shapes in z-index order and filter to candidates that pass isPointInShape.
6109
+ // Frame-like shapes are always checked because their labels can be outside their bounds.
6110
+ // Iterate backwards so the result is pre-sorted in reverse z-index order (top-most first).
6111
+ const sorted = this.getCurrentPageShapesSorted()
6112
+ const result: TLShape[] = []
6113
+ for (let i = sorted.length - 1; i >= 0; i--) {
6114
+ const shape = sorted[i]
6115
+ if (this.isShapeHidden(shape)) continue
6116
+ if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
6117
+ if (this.isPointInShape(shape, point, opts)) result.push(shape)
6118
+ }
6119
+ return result
6126
6120
  }
6127
6121
 
6128
6122
  /**
@@ -6222,9 +6216,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6222
6216
  if (!freshShape) return new Vec(0, 0)
6223
6217
  if (isPageId(freshShape.parentId)) return Vec.From(point)
6224
6218
 
6225
- const parentTransform = this.getShapePageTransform(freshShape.parentId)
6226
- if (!parentTransform) return Vec.From(point)
6227
- return parentTransform.clone().invert().applyToPoint(point)
6219
+ return this.getShapePageTransform(freshShape.parentId).clone().invert().applyToPoint(point)
6228
6220
  }
6229
6221
 
6230
6222
  /**
@@ -6391,26 +6383,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6391
6383
  * @public
6392
6384
  */
6393
6385
  isShapeInPage(shape: TLShape | TLShapeId, pageId = this.getCurrentPageId()): boolean {
6394
- const id = typeof shape === 'string' ? shape : shape.id
6395
- const shapeToCheck = this.getShape(id)
6396
- if (!shapeToCheck) return false
6397
-
6398
- let shapeIsInPage = false
6399
-
6400
- if (shapeToCheck.parentId === pageId) {
6401
- shapeIsInPage = true
6402
- } else {
6403
- let parent = this.getShape(shapeToCheck.parentId)
6404
- isInPageSearch: while (parent) {
6405
- if (parent.parentId === pageId) {
6406
- shapeIsInPage = true
6407
- break isInPageSearch
6408
- }
6409
- parent = this.getShape(parent.parentId)
6410
- }
6411
- }
6412
-
6413
- return shapeIsInPage
6386
+ return this.getAncestorPageId(shape) === pageId
6414
6387
  }
6415
6388
 
6416
6389
  /**
@@ -6521,13 +6494,8 @@ export class Editor extends EventEmitter<TLEventMap> {
6521
6494
  for (let i = 0; i < shapesToReparent.length; i++) {
6522
6495
  const shape = shapesToReparent[i]
6523
6496
 
6524
- const pageTransform = this.getShapePageTransform(shape)!
6525
- if (!pageTransform) continue
6526
-
6527
- const pagePoint = pageTransform.point()
6528
- if (!pagePoint) continue
6529
-
6530
- const newPoint = invertedParentTransform.applyToPoint(pagePoint)
6497
+ const pageTransform = this.getShapePageTransform(shape)
6498
+ const newPoint = invertedParentTransform.applyToPoint(pageTransform.point())
6531
6499
  const newRotation = pageTransform.rotation() - parentPageRotation
6532
6500
 
6533
6501
  if (shape.id === parentId) {
@@ -6628,7 +6596,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6628
6596
  */
6629
6597
  getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId> {
6630
6598
  const shapeIds = new Set<TLShapeId>()
6631
- for (const shape of ids.map((id) => this.getShape(id)!).sort(sortByIndex)) {
6599
+ for (const shape of compact(ids.map((id) => this.getShape(id))).sort(sortByIndex)) {
6632
6600
  shapeIds.add(shape.id)
6633
6601
  this.visitDescendants(shape, (descendantId) => {
6634
6602
  shapeIds.add(descendantId)
@@ -6652,6 +6620,9 @@ export class Editor extends EventEmitter<TLEventMap> {
6652
6620
  const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
6653
6621
  (s) => !s.isLocked && !this.isShapeHidden(s)
6654
6622
  )
6623
+ // Descendants of the dragged shapes can't be the target, otherwise dragging a frame out of
6624
+ // its parent reports a nested child as the target and the parent never sees the drag leave
6625
+ const excludedIds = this.getShapeAndDescendantIds(draggingShapes.map((s) => s.id))
6655
6626
 
6656
6627
  const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
6657
6628
  hitInside: true,
@@ -6661,7 +6632,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6661
6632
  !droppingShapes.includes(s) &&
6662
6633
  !s.isLocked &&
6663
6634
  !this.isShapeHidden(s) &&
6664
- !draggingShapes.includes(s)
6635
+ !excludedIds.has(s.id)
6665
6636
  )
6666
6637
 
6667
6638
  for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
@@ -7035,8 +7006,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7035
7006
  for (const id of ids) {
7036
7007
  const shape = this.getShape(id)!
7037
7008
  const localDelta = Vec.From(offset)
7038
- const parentTransform = this.getShapeParentTransform(shape)
7039
- if (parentTransform) localDelta.rot(-parentTransform.rotation())
7009
+ localDelta.rot(-this.getShapeParentTransform(shape).rotation())
7040
7010
 
7041
7011
  changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)))
7042
7012
  }
@@ -7106,7 +7076,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7106
7076
  let ox = 0
7107
7077
  let oy = 0
7108
7078
 
7109
- if (offset && initialIds.has(originalId)) {
7079
+ // Only offset the roots of the duplicated tree: a descendant that was also passed in
7080
+ // follows its duplicated parent, so offsetting it too would move it twice
7081
+ if (
7082
+ offset &&
7083
+ initialIds.has(originalId) &&
7084
+ !shapeIdSet.has(originalShape.parentId as TLShapeId)
7085
+ ) {
7110
7086
  const parentTransform = this.getShapeParentTransform(originalShape)
7111
7087
  const vec = new Vec(offset.x, offset.y).rot(-parentTransform!.rotation())
7112
7088
  ox = vec.x
@@ -7272,27 +7248,17 @@ export class Editor extends EventEmitter<TLEventMap> {
7272
7248
 
7273
7249
  if (this.getIsReadonly() || ids.length === 0) return this
7274
7250
 
7275
- let allLocked = true,
7276
- allUnlocked = true
7251
+ let allLocked = true
7277
7252
  const shapesToToggle: TLShape[] = []
7278
7253
  for (const id of ids) {
7279
7254
  const shape = this.getShape(id)
7280
7255
  if (shape) {
7281
7256
  shapesToToggle.push(shape)
7282
- if (shape.isLocked) {
7283
- allUnlocked = false
7284
- } else {
7285
- allLocked = false
7286
- }
7257
+ if (!shape.isLocked) allLocked = false
7287
7258
  }
7288
7259
  }
7289
7260
  this.run(() => {
7290
- if (allUnlocked) {
7291
- this.updateShapes(
7292
- shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
7293
- )
7294
- this.setSelectedShapes([])
7295
- } else if (allLocked) {
7261
+ if (allLocked) {
7296
7262
  this.updateShapes(
7297
7263
  shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
7298
7264
  )
@@ -7300,6 +7266,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7300
7266
  this.updateShapes(
7301
7267
  shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
7302
7268
  )
7269
+ this.setSelectedShapes([])
7303
7270
  }
7304
7271
  })
7305
7272
 
@@ -7439,9 +7406,12 @@ export class Editor extends EventEmitter<TLEventMap> {
7439
7406
 
7440
7407
  // optionally filter to axis-aligned shapes (rotation is a multiple of 90 degrees)
7441
7408
  if (opts?.filterAxisAligned) {
7442
- freshShapes = freshShapes.filter(
7443
- (s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
7444
- )
7409
+ freshShapes = freshShapes.filter((s) => {
7410
+ // Page rotations come back through atan2 on a composed matrix, so a 270° shape can land
7411
+ // a hair either side of the multiple; an exact === 0 check drops it
7412
+ const remainder = Math.abs(this.getShapePageTransform(s).rotation() % HALF_PI)
7413
+ return Math.min(remainder, HALF_PI - remainder) < 1e-9
7414
+ })
7445
7415
  }
7446
7416
 
7447
7417
  const clusters: { shapes: TLShape[]; pageBounds: Box }[] = []
@@ -7662,13 +7632,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7662
7632
 
7663
7633
  let shapeGap: number = 0
7664
7634
 
7635
+ // Stack in spatial order rather than input (z) order, otherwise shapes swap places
7636
+ shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
7637
+
7665
7638
  if (_gap === 0) {
7666
7639
  // note: this is not used in the current tldraw.com; there we use a specified stack
7667
7640
 
7668
7641
  const gaps: Record<number, number> = {}
7669
7642
 
7670
- shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
7671
-
7672
7643
  // Collect all of the gaps between shapes. We want to find
7673
7644
  // patterns (equal gaps between shapes) and use the most common
7674
7645
  // one as the gap for all of the shapes.
@@ -7721,8 +7692,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7721
7692
  // todo: ensure that the parent isn't being aligned together with its children
7722
7693
  const parent = this.getShapeParent(shape)
7723
7694
  if (parent) {
7724
- const parentTransform = this.getShapePageTransform(parent)
7725
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7695
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
7726
7696
  }
7727
7697
 
7728
7698
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -7772,10 +7742,11 @@ export class Editor extends EventEmitter<TLEventMap> {
7772
7742
 
7773
7743
  const maxWidth = commonBounds.width
7774
7744
 
7775
- // sort the shape clusters by width and then height, descending
7745
+ // sort the shape clusters by width and then height, descending: potpack fills each row's
7746
+ // right-hand space with the shapes that follow, so they must be no taller than the row
7776
7747
  shapeClustersToPack
7777
- .sort((a, b) => a.pageBounds.width - b.pageBounds.width)
7778
- .sort((a, b) => a.pageBounds.height - b.pageBounds.height)
7748
+ .sort((a, b) => b.pageBounds.width - a.pageBounds.width)
7749
+ .sort((a, b) => b.pageBounds.height - a.pageBounds.height)
7779
7750
 
7780
7751
  // Start with is (sort of) the square of the area
7781
7752
  const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
@@ -7845,8 +7816,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7845
7816
 
7846
7817
  const parent = this.getShapeParent(shape)
7847
7818
  if (parent) {
7848
- const parentTransform = this.getShapeParentTransform(shape)
7849
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7819
+ shapeDelta.rot(-this.getShapeParentTransform(shape).rotation())
7850
7820
  }
7851
7821
 
7852
7822
  shapeDelta.add(shape)
@@ -7936,8 +7906,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7936
7906
  // todo: ensure that the parent isn't being aligned together with its children
7937
7907
  const parent = this.getShapeParent(shape)
7938
7908
  if (parent) {
7939
- const parentTransform = this.getShapePageTransform(parent)
7940
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7909
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
7941
7910
  }
7942
7911
 
7943
7912
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -8037,8 +8006,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8037
8006
  // todo: ensure that the parent isn't being aligned together with its children
8038
8007
  const parent = this.getShapeParent(shape)
8039
8008
  if (parent) {
8040
- const parentTransform = this.getShapePageTransform(parent)
8041
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
8009
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
8042
8010
  }
8043
8011
 
8044
8012
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -8106,8 +8074,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8106
8074
  for (const shape of shapes) {
8107
8075
  // First translate
8108
8076
  const shapeLocalOffset = localOffset.clone()
8109
- const parentTransform = this.getShapeParentTransform(shape)
8110
- if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
8077
+ shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
8111
8078
  shapeLocalOffset.add(shape)
8112
8079
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
8113
8080
  this.updateShape(changes)
@@ -8178,8 +8145,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8178
8145
  for (const shape of shapes) {
8179
8146
  // First translate
8180
8147
  const shapeLocalOffset = localOffset.clone()
8181
- const parentTransform = this.getShapeParentTransform(shape)
8182
- if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
8148
+ shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
8183
8149
  shapeLocalOffset.add(shape)
8184
8150
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
8185
8151
  this.updateShape(changes)
@@ -8243,12 +8209,9 @@ export class Editor extends EventEmitter<TLEventMap> {
8243
8209
  const pageTransform = opts.initialPageTransform
8244
8210
  ? Mat.Cast(opts.initialPageTransform)
8245
8211
  : this.getShapePageTransform(id)
8246
- if (!pageTransform) return null
8247
8212
 
8248
8213
  const pageRotation = pageTransform.rotation()
8249
8214
 
8250
- if (pageRotation == null) return null
8251
-
8252
8215
  const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
8253
8216
 
8254
8217
  const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
@@ -8485,7 +8448,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8485
8448
  )
8486
8449
 
8487
8450
  // now calculate how far away the shape is from where it needs to be
8488
- const pageTransform = this.getShapePageTransform(id)!
8451
+ const pageTransform = this.getShapePageTransform(id)
8489
8452
  // We need to use the local bounds center transformed to page space, not the axis-aligned
8490
8453
  // page bounds center. This is because the page bounds are axis-aligned and their center
8491
8454
  // changes when the rotation changes, but we want to use the same reference point as
@@ -8493,7 +8456,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8493
8456
  const currentLocalBounds = this.getShapeGeometry(id).bounds
8494
8457
  const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
8495
8458
  const shapePageTransformOrigin = pageTransform.point()
8496
- if (!currentPageCenter || !shapePageTransformOrigin) return this
8497
8459
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
8498
8460
 
8499
8461
  // and finally figure out what the shape's new position should be
@@ -8881,10 +8843,10 @@ export class Editor extends EventEmitter<TLEventMap> {
8881
8843
 
8882
8844
  updates.push({
8883
8845
  ...end,
8884
- x: start.x + (end.x - start.x) * t,
8885
- y: start.y + (end.y - start.y) * t,
8886
- opacity: start.opacity + (end.opacity - start.opacity) * t,
8887
- rotation: start.rotation + (end.rotation - start.rotation) * t,
8846
+ x: lerp(start.x, end.x, t),
8847
+ y: lerp(start.y, end.y, t),
8848
+ opacity: lerp(start.opacity, end.opacity, t),
8849
+ rotation: lerp(start.rotation, end.rotation, t),
8888
8850
  props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props,
8889
8851
  })
8890
8852
  }
@@ -9100,7 +9062,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9100
9062
  * @public
9101
9063
  */
9102
9064
  updateShapes<T extends TLShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
9103
- const compactedPartials: TLShapePartial<T>[] = Array(partials.length)
9065
+ const compactedPartials: TLShapePartial<T>[] = []
9104
9066
 
9105
9067
  for (let i = 0, n = partials.length; i < n; i++) {
9106
9068
  const partial = partials[i]
@@ -9246,26 +9208,25 @@ export class Editor extends EventEmitter<TLEventMap> {
9246
9208
  /* --------------------- Styles --------------------- */
9247
9209
 
9248
9210
  /**
9249
- * Get all the current styles among the users selected shapes
9211
+ * Groups have no styles of their own: a style read or write on a selection applies to the
9212
+ * non-group shapes beneath each group, however deeply nested. Returns those shapes.
9250
9213
  *
9251
9214
  * @internal
9252
9215
  */
9253
- private _extractSharedStyles(shape: TLShape, sharedStyleMap: SharedStyleMap) {
9254
- if (this.isShapeOfType(shape, 'group')) {
9255
- // For groups, ignore the styles of the group shape and instead include the styles of the
9256
- // group's children. These are the shapes that would have their styles changed if the
9257
- // user called `setStyle` on the current selection.
9258
- const childIds = this._parentIdsToChildIds.get()[shape.id]
9259
- if (!childIds) return
9260
-
9261
- for (let i = 0, n = childIds.length; i < n; i++) {
9262
- this._extractSharedStyles(this.getShape(childIds[i])!, sharedStyleMap)
9263
- }
9264
- } else {
9265
- for (const [style, propKey] of this.styleProps[shape.type]) {
9266
- sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey))
9216
+ private _getStyleableShapes(shapes: TLShape[]): TLShape[] {
9217
+ const result: TLShape[] = []
9218
+ const visit = (shape: TLShape) => {
9219
+ if (this.isShapeOfType(shape, 'group')) {
9220
+ for (const childId of this.getSortedChildIdsForParent(shape.id)) {
9221
+ const child = this.getShape(childId)
9222
+ if (child) visit(child)
9223
+ }
9224
+ } else {
9225
+ result.push(shape)
9267
9226
  }
9268
9227
  }
9228
+ for (const shape of shapes) visit(shape)
9229
+ return result
9269
9230
  }
9270
9231
 
9271
9232
  /**
@@ -9275,11 +9236,11 @@ export class Editor extends EventEmitter<TLEventMap> {
9275
9236
  */
9276
9237
  @computed
9277
9238
  private _getSelectionSharedStyles(): ReadonlySharedStyleMap {
9278
- const selectedShapes = this.getSelectedShapes()
9279
-
9280
9239
  const sharedStyles = new SharedStyleMap()
9281
- for (const selectedShape of selectedShapes) {
9282
- this._extractSharedStyles(selectedShape, sharedStyles)
9240
+ for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
9241
+ for (const [style, propKey] of this.styleProps[shape.type]) {
9242
+ sharedStyles.applyValue(style, getOwnProperty(shape.props, propKey))
9243
+ }
9283
9244
  }
9284
9245
 
9285
9246
  return sharedStyles
@@ -9337,18 +9298,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9337
9298
  if (!currentTool) return styles
9338
9299
 
9339
9300
  if (currentTool.shapeType) {
9340
- if (
9341
- currentTool.shapeType === 'frame' &&
9342
- !(this.getShapeUtil('frame')!.options as any).showColors
9343
- ) {
9344
- for (const style of this.styleProps[currentTool.shapeType].keys()) {
9345
- if (style.id === 'tldraw:color') continue
9346
- styles.applyValue(style, this.getStyleForNextShape(style))
9347
- }
9348
- } else {
9349
- for (const style of this.styleProps[currentTool.shapeType].keys()) {
9350
- styles.applyValue(style, this.getStyleForNextShape(style))
9351
- }
9301
+ for (const style of this.styleProps[currentTool.shapeType].keys()) {
9302
+ styles.applyValue(style, this.getStyleForNextShape(style))
9352
9303
  }
9353
9304
  }
9354
9305
 
@@ -9364,27 +9315,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9364
9315
  */
9365
9316
  @computed getSharedOpacity(): SharedStyle<number> {
9366
9317
  if (this.isIn('select') && this.getSelectedShapeIds().length > 0) {
9367
- const shapesToCheck: TLShape[] = []
9368
- const addShape = (shapeId: TLShapeId) => {
9369
- const shape = this.getShape(shapeId)
9370
- if (!shape) return
9371
- // For groups, ignore the opacity of the group shape and instead include
9372
- // the opacity of the group's children. These are the shapes that would have
9373
- // their opacity changed if the user called `setOpacity` on the current selection.
9374
- if (this.isShapeOfType(shape, 'group')) {
9375
- for (const childId of this.getSortedChildIdsForParent(shape.id)) {
9376
- addShape(childId)
9377
- }
9378
- } else {
9379
- shapesToCheck.push(shape)
9380
- }
9381
- }
9382
- for (const shapeId of this.getSelectedShapeIds()) {
9383
- addShape(shapeId)
9384
- }
9385
-
9386
9318
  let opacity: number | null = null
9387
- for (const shape of shapesToCheck) {
9319
+ for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
9388
9320
  if (opacity === null) {
9389
9321
  opacity = shape.opacity
9390
9322
  } else if (opacity !== shape.opacity) {
@@ -9427,33 +9359,12 @@ export class Editor extends EventEmitter<TLEventMap> {
9427
9359
  const selectedShapes = this.getSelectedShapes()
9428
9360
 
9429
9361
  if (selectedShapes.length > 0) {
9430
- const shapesToUpdate: TLShape[] = []
9431
-
9432
- // We can have many deep levels of grouped shape
9433
- // Making a recursive function to look through all the levels
9434
- const addShapeById = (shape: TLShape) => {
9435
- if (this.isShapeOfType(shape, 'group')) {
9436
- const childIds = this.getSortedChildIdsForParent(shape)
9437
- for (const childId of childIds) {
9438
- addShapeById(this.getShape(childId)!)
9439
- }
9440
- } else {
9441
- shapesToUpdate.push(shape)
9442
- }
9443
- }
9444
-
9445
- for (const id of selectedShapes) {
9446
- addShapeById(id)
9447
- }
9448
-
9449
9362
  this.updateShapes(
9450
- shapesToUpdate.map((shape) => {
9451
- return {
9452
- id: shape.id,
9453
- type: shape.type,
9454
- opacity,
9455
- }
9456
- })
9363
+ this._getStyleableShapes(selectedShapes).map((shape) => ({
9364
+ id: shape.id,
9365
+ type: shape.type,
9366
+ opacity,
9367
+ }))
9457
9368
  )
9458
9369
  }
9459
9370
 
@@ -9507,43 +9418,19 @@ export class Editor extends EventEmitter<TLEventMap> {
9507
9418
  const selectedShapes = this.getSelectedShapes()
9508
9419
 
9509
9420
  if (selectedShapes.length > 0) {
9510
- const updates: {
9511
- util: ShapeUtil
9512
- originalShape: TLShape
9513
- updatePartial: TLShapePartial
9514
- }[] = []
9515
-
9516
- // We can have many deep levels of grouped shape
9517
- // Making a recursive function to look through all the levels
9518
- const addShapeById = (shape: TLShape) => {
9519
- if (this.isShapeOfType(shape, 'group')) {
9520
- const childIds = this.getSortedChildIdsForParent(shape.id)
9521
- for (const childId of childIds) {
9522
- addShapeById(this.getShape(childId)!)
9523
- }
9524
- } else {
9525
- const util = this.getShapeUtil(shape)
9526
- const stylePropKey = this.styleProps[shape.type].get(style)
9527
- if (stylePropKey) {
9528
- const shapePartial: TLShapePartial = {
9529
- id: shape.id,
9530
- type: shape.type,
9531
- props: { [stylePropKey]: value },
9532
- }
9533
- updates.push({
9534
- util,
9535
- originalShape: shape,
9536
- updatePartial: shapePartial,
9537
- })
9538
- }
9421
+ const updates: TLShapePartial[] = []
9422
+ for (const shape of this._getStyleableShapes(selectedShapes)) {
9423
+ const stylePropKey = this.styleProps[shape.type].get(style)
9424
+ if (stylePropKey) {
9425
+ updates.push({
9426
+ id: shape.id,
9427
+ type: shape.type,
9428
+ props: { [stylePropKey]: value },
9429
+ })
9539
9430
  }
9540
9431
  }
9541
9432
 
9542
- for (const shape of selectedShapes) {
9543
- addShapeById(shape)
9544
- }
9545
-
9546
- this.updateShapes(updates.map(({ updatePartial }) => updatePartial))
9433
+ this.updateShapes(updates)
9547
9434
  }
9548
9435
 
9549
9436
  return this
@@ -10075,7 +9962,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10075
9962
  if (shapeIdMap.has(newShape.parentId)) {
10076
9963
  newShape.parentId = shapeIdMap.get(oldShape.parentId)!
10077
9964
  } else {
10078
- rootShapeIds.push(newShape.id)
10079
9965
  // newShape.parentId = pasteParentId
10080
9966
  newShape.index = index
10081
9967
  index = getIndexAbove(index)
@@ -10118,10 +10004,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10118
10004
  (asset.type === 'video' && asset.props.src?.startsWith('data:video'))
10119
10005
  ) {
10120
10006
  // it's src is a base64 image or video; we need to create a new asset without the src,
10121
- // then create a new asset from the original src. So we save a copy of the original asset,
10122
- // then delete the src from the original asset.
10123
- assetsToUpdate.push(structuredClone(asset as TLImageAsset | TLVideoAsset))
10124
- asset.props.src = null
10007
+ // then create a new asset from the original src. So we keep the original asset for the
10008
+ // upload and create a copy with its src removed. Copy rather than mutate: when no
10009
+ // migration applies, migrateStoreSnapshot hands back the caller's own record objects.
10010
+ assetsToUpdate.push(asset as TLImageAsset | TLVideoAsset)
10011
+ const assetWithoutSrc = structuredClone(asset as TLImageAsset | TLVideoAsset)
10012
+ assetWithoutSrc.props.src = null
10013
+ assetsToCreate.push(assetWithoutSrc)
10014
+ continue
10125
10015
  }
10126
10016
 
10127
10017
  // Add the asset to the list of assets to create
@@ -11116,7 +11006,10 @@ export class Editor extends EventEmitter<TLEventMap> {
11116
11006
  if (info.name === 'cancel' || info.name === 'complete') {
11117
11007
  this.inputs.setIsDragging(false)
11118
11008
 
11119
- if (this.inputs.getIsPanning()) {
11009
+ // A pan owned by a held spacebar outlives the cancelled interaction;
11010
+ // key_up ends it. Otherwise Escape mid-pan stops the camera until
11011
+ // the user releases and re-presses Space (#10446).
11012
+ if (this.inputs.getIsPanning() && !this.inputs.keys.has('Space')) {
11120
11013
  this.inputs.setIsPanning(false)
11121
11014
  this.inputs.setIsSpacebarPanning(false)
11122
11015
  this.setCursor({ type: this._prevCursor, rotation: 0 })
@@ -11870,10 +11763,10 @@ function withIsolatedShapes<T>(
11870
11763
 
11871
11764
  function getCameraFitXFitY(editor: Editor, cameraOptions: TLCameraOptions) {
11872
11765
  if (!cameraOptions.constraints) throw Error('Should have constraints here')
11873
- const {
11874
- padding: { x: px, y: py },
11875
- } = cameraOptions.constraints
11876
11766
  const vsb = editor.getViewportScreenBounds()
11767
+ // Clamp padding to half the viewport size on either dimension, as getConstrainedCamera does
11768
+ const px = Math.min(cameraOptions.constraints.padding.x, vsb.w / 2)
11769
+ const py = Math.min(cameraOptions.constraints.padding.y, vsb.h / 2)
11877
11770
  const bounds = Box.From(cameraOptions.constraints.bounds)
11878
11771
  const zx = (vsb.w - px * 2) / bounds.w
11879
11772
  const zy = (vsb.h - py * 2) / bounds.h