@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
@@ -39,9 +39,13 @@ export function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: ()
39
39
  function fromScratch() {
40
40
  const currentPageId = getCurrentPageId()
41
41
  lastPageId = currentPageId
42
- return new Set(
43
- [...shapesIndex.get()].filter((id) => isShapeInPage(store, currentPageId, store.get(id)!))
44
- )
42
+ const result = new Set<TLShapeId>()
43
+ for (const id of shapesIndex.get()) {
44
+ if (isShapeInPage(store, currentPageId, store.get(id)!)) {
45
+ result.add(id)
46
+ }
47
+ }
48
+ return result
45
49
  }
46
50
  return computed<Set<TLShapeId>>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => {
47
51
  if (isUninitialized(prevValue)) {
@@ -94,7 +94,7 @@ export class ClickManager {
94
94
  switch (this._clickState) {
95
95
  case 'pendingDouble': {
96
96
  this._clickState = 'pendingOverflow'
97
- this._clickTimeout = this._getClickTimeout(this._clickState)
97
+ this._getClickTimeout(this._clickState)
98
98
  return {
99
99
  ...info,
100
100
  type: 'click',
@@ -114,7 +114,7 @@ export class ClickManager {
114
114
  // overflow
115
115
  }
116
116
  }
117
- this._clickTimeout = this._getClickTimeout(this._clickState)
117
+ this._getClickTimeout(this._clickState)
118
118
  return info
119
119
  }
120
120
  case 'pointer_up': {
@@ -141,10 +141,12 @@ export class ClickManager {
141
141
  return info
142
142
  }
143
143
  case 'pointer_move': {
144
+ // Compare in client coordinates like _clickScreenPoint; the container-relative
145
+ // inputs screen point would add the container offset and cancel on any movement.
144
146
  if (
145
147
  this._clickState !== 'idle' &&
146
148
  this._clickScreenPoint &&
147
- Vec.Dist2(this._clickScreenPoint, this.editor.inputs.getCurrentScreenPoint()) >
149
+ Vec.Dist2(this._clickScreenPoint, info.point) >
148
150
  (this.editor.getInstanceState().isCoarsePointer
149
151
  ? this.editor.options.coarseDragDistanceSquared
150
152
  : this.editor.options.dragDistanceSquared)
@@ -1,6 +1,6 @@
1
1
  import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
2
2
  import type { TLInstancePresence } from '@tldraw/tlschema'
3
- import { areArraysShallowEqual, maxBy } from '@tldraw/utils'
3
+ import { areArraysShallowEqual } from '@tldraw/utils'
4
4
  import type { Editor } from '../../Editor'
5
5
 
6
6
  /**
@@ -54,14 +54,14 @@ export class CollaboratorsManager {
54
54
  getCollaborators(): TLInstancePresence[] {
55
55
  const allPresenceRecords = this._getCollaboratorsQuery().get()
56
56
  if (!allPresenceRecords.length) return EMPTY_ARRAY
57
- const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort()
58
- return userIds.map((id) => {
59
- const latestPresence = maxBy(
60
- allPresenceRecords.filter((c) => c.userId === id),
61
- (p) => p.lastActivityTimestamp ?? 0
62
- )
63
- return latestPresence!
64
- })
57
+ const latestByUserId = new Map<string, TLInstancePresence>()
58
+ for (const presence of allPresenceRecords) {
59
+ const latest = latestByUserId.get(presence.userId)
60
+ if (!latest || (presence.lastActivityTimestamp ?? 0) > (latest.lastActivityTimestamp ?? 0)) {
61
+ latestByUserId.set(presence.userId, presence)
62
+ }
63
+ }
64
+ return [...latestByUserId.keys()].sort().map((id) => latestByUserId.get(id)!)
65
65
  }
66
66
 
67
67
  /**
@@ -262,15 +262,46 @@ describe('EdgeScrollManager', () => {
262
262
  })
263
263
 
264
264
  it('should adjust scroll speed based on zoom level', () => {
265
- editor.getZoomLevel.mockReturnValue(2)
266
265
  mockInputs.setCurrentScreenPoint(new Vec(5, 300))
267
266
 
268
267
  edgeScrollManager.updateEdgeScrolling(300)
268
+ const atZoom1 = (editor.setCamera.mock.calls[0][0] as Vec).x
269
+
270
+ editor.setCamera.mockClear()
271
+ edgeScrollManager = new EdgeScrollManager(editor)
272
+ editor.getZoomLevel.mockReturnValue(2)
273
+ edgeScrollManager.updateEdgeScrolling(300)
274
+ const atZoom2 = (editor.setCamera.mock.calls[0][0] as Vec).x
269
275
 
270
- expect(editor.setCamera).toHaveBeenCalled()
271
- const callArgs = editor.setCamera.mock.calls[0][0] as Vec
272
276
  // Higher zoom should result in smaller camera movement
273
- expect(Math.abs(callArgs.x)).toBeLessThan(25)
277
+ expect(atZoom2).toBeCloseTo(atZoom1 / 2)
278
+ })
279
+
280
+ it('should move the same distance per second regardless of tick rate', () => {
281
+ mockInputs.setCurrentScreenPoint(new Vec(5, 300))
282
+
283
+ // Get past the delay and easing ramp so each tick moves at full speed
284
+ edgeScrollManager.updateEdgeScrolling(1000)
285
+ editor.setCamera.mockClear()
286
+
287
+ // One second of 60 Hz ticks
288
+ for (let i = 0; i < 60; i++) edgeScrollManager.updateEdgeScrolling(1000 / 60)
289
+ const at60Hz = editor.setCamera.mock.calls.reduce(
290
+ (sum, [camera]) => sum + (camera as Vec).x,
291
+ 0
292
+ )
293
+
294
+ editor.setCamera.mockClear()
295
+
296
+ // One second of 120 Hz ticks
297
+ for (let i = 0; i < 120; i++) edgeScrollManager.updateEdgeScrolling(1000 / 120)
298
+ const at120Hz = editor.setCamera.mock.calls.reduce(
299
+ (sum, [camera]) => sum + (camera as Vec).x,
300
+ 0
301
+ )
302
+
303
+ expect(at60Hz).toBeGreaterThan(0)
304
+ expect(at120Hz).toBeCloseTo(at60Hz)
274
305
  })
275
306
 
276
307
  it('should add scroll delta to current camera position', () => {
@@ -1,3 +1,4 @@
1
+ import { FRAME_MS_60HZ } from '../../../constants'
1
2
  import { EASINGS } from '../../../primitives/easings'
2
3
  import { Vec } from '../../../primitives/Vec'
3
4
  import type { Editor } from '../../Editor'
@@ -51,10 +52,13 @@ export class EdgeScrollManager {
51
52
  )
52
53
  )
53
54
  : 1
54
- this.moveCameraWhenCloseToEdge({
55
- x: edgeScrollProximityFactor.x * eased,
56
- y: edgeScrollProximityFactor.y * eased,
57
- })
55
+ this.moveCameraWhenCloseToEdge(
56
+ {
57
+ x: edgeScrollProximityFactor.x * eased,
58
+ y: edgeScrollProximityFactor.y * eased,
59
+ },
60
+ elapsed
61
+ )
58
62
  }
59
63
  }
60
64
  }
@@ -112,7 +116,7 @@ export class EdgeScrollManager {
112
116
  * Moves the camera when the mouse is close to the edge of the screen.
113
117
  * @public
114
118
  */
115
- private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
119
+ private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }, elapsed: number) {
116
120
  if (proximityFactor.x === 0 && proximityFactor.y === 0) return
117
121
  const { editor } = this
118
122
 
@@ -128,9 +132,11 @@ export class EdgeScrollManager {
128
132
  ? EDGE_SCROLL_SMALL_SCREEN_SPEED_FACTOR
129
133
  : 1
130
134
 
131
- // Determines the base speed of the scroll
135
+ // Determines the base speed of the scroll. edgeScrollSpeed is px per 60 Hz frame, so scale
136
+ // by elapsed time or a 120 Hz display scrolls twice as fast.
132
137
  const zoomLevel = editor.getZoomLevel()
133
- const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed
138
+ const pxSpeed =
139
+ editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed * (elapsed / FRAME_MS_60HZ)
134
140
  const scrollDeltaX = (pxSpeed * proximityFactor.x * screenSizeFactorX) / zoomLevel
135
141
  const scrollDeltaY = (pxSpeed * proximityFactor.y * screenSizeFactorY) / zoomLevel
136
142
 
@@ -8,6 +8,7 @@ import {
8
8
  mapObjectMapValues,
9
9
  objectMapEntries,
10
10
  } from '@tldraw/utils'
11
+ import { areShapesContentEqual } from '../../../utils/areShapesContentEqual'
11
12
  import type { Editor } from '../../Editor'
12
13
 
13
14
  interface FontState {
@@ -41,7 +42,7 @@ export class FontManager {
41
42
  },
42
43
  {
43
44
  areResultsEqual: areArraysShallowEqual,
44
- areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
45
+ areRecordsEqual: areShapesContentEqual,
45
46
  }
46
47
  )
47
48
 
@@ -1,16 +1,39 @@
1
+ import { InstancePresenceRecordType, TLPOINTER_ID, createUserId } from '@tldraw/tlschema'
2
+ import { vi } from 'vitest'
1
3
  import { createTLStore } from '../../../config/createTLStore'
4
+ import { TldrawOptions } from '../../../options'
2
5
  import { Editor } from '../../Editor'
6
+ import { StateNode } from '../../tools/StateNode'
3
7
 
4
- function createTestEditor() {
8
+ function createPresence(editor: Editor) {
9
+ return InstancePresenceRecordType.create({
10
+ id: InstancePresenceRecordType.createId('peer'),
11
+ userId: createUserId('peer'),
12
+ userName: 'Peer',
13
+ currentPageId: editor.getCurrentPageId(),
14
+ })
15
+ }
16
+
17
+ function createTestEditor(options?: Partial<TldrawOptions>) {
5
18
  const store = createTLStore({})
6
19
  store.ensureStoreIsUsable()
7
- return new Editor({
20
+ // Attached to the document so that events dispatched on the container
21
+ // propagate to the window, where the activity listeners live.
22
+ const container = document.createElement('div')
23
+ document.body.appendChild(container)
24
+ const editor = new Editor({
8
25
  store,
9
26
  bindingUtils: [],
10
27
  shapeUtils: [],
11
- getContainer: () => document.createElement('div'),
28
+ getContainer: () => container,
12
29
  tools: [],
30
+ options,
13
31
  })
32
+ editor.disposables.add(() => container.remove())
33
+ // Input listeners only attach once the editor is mounted, which is normally
34
+ // emitted by `TldrawEditor` when the container is in the DOM.
35
+ editor.emit('mount')
36
+ return editor
14
37
  }
15
38
 
16
39
  describe('InputsManager', () => {
@@ -37,6 +60,179 @@ describe('InputsManager', () => {
37
60
  expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
38
61
  })
39
62
 
63
+ describe('markActivity', () => {
64
+ beforeEach(() => {
65
+ vi.useFakeTimers()
66
+ })
67
+
68
+ afterEach(() => {
69
+ vi.useRealTimers()
70
+ })
71
+
72
+ it('does nothing when there are no collaborators', () => {
73
+ editor.inputs.markActivity()
74
+
75
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(0)
76
+ })
77
+
78
+ it('stamps the pointer record, preserving its position', () => {
79
+ editor.store.put([createPresence(editor)])
80
+ editor.store.put([{ ...editor.store.get(TLPOINTER_ID)!, x: 5, y: 6 }])
81
+
82
+ editor.inputs.markActivity()
83
+
84
+ expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
85
+ x: 5,
86
+ y: 6,
87
+ lastActivityTimestamp: Date.now(),
88
+ })
89
+ })
90
+
91
+ it('throttles stamps on the leading edge', () => {
92
+ editor.store.put([createPresence(editor)])
93
+
94
+ editor.inputs.markActivity()
95
+ const firstStamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
96
+
97
+ vi.advanceTimersByTime(500)
98
+ editor.inputs.markActivity()
99
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp)
100
+
101
+ vi.advanceTimersByTime(600)
102
+ editor.inputs.markActivity()
103
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp + 1100)
104
+ })
105
+
106
+ it('stamps on keyboard input in the container', () => {
107
+ editor.store.put([createPresence(editor)])
108
+
109
+ editor.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
110
+
111
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
112
+ })
113
+
114
+ it('stamps on pointer input in the container', () => {
115
+ editor.store.put([createPresence(editor)])
116
+
117
+ editor.getContainer().dispatchEvent(new Event('pointermove', { bubbles: true }))
118
+
119
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
120
+ })
121
+
122
+ it('stamps on text input that fires no keydown, e.g. IME and gesture typing', () => {
123
+ editor.store.put([createPresence(editor)])
124
+
125
+ editor.getContainer().dispatchEvent(new Event('beforeinput', { bubbles: true }))
126
+
127
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
128
+ })
129
+
130
+ it('stamps on input elsewhere in the tab, outside the container', () => {
131
+ editor.store.put([createPresence(editor)])
132
+
133
+ document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
134
+
135
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
136
+ })
137
+
138
+ it('keeps a minimum throttle window when the idle timeout is tiny', () => {
139
+ const tinyEditor = createTestEditor({ collaboratorIdleTimeoutMs: 0 })
140
+ try {
141
+ tinyEditor.store.put([createPresence(tinyEditor)])
142
+
143
+ tinyEditor.inputs.markActivity()
144
+ const stamp = tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
145
+
146
+ vi.advanceTimersByTime(50)
147
+ tinyEditor.inputs.markActivity()
148
+ expect(tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(stamp)
149
+ } finally {
150
+ tinyEditor.dispose()
151
+ }
152
+ })
153
+
154
+ it('attaches input listeners only while sharing with someone', () => {
155
+ const addEventListener = vi.spyOn(window, 'addEventListener')
156
+ const removeEventListener = vi.spyOn(window, 'removeEventListener')
157
+ const soloEditor = createTestEditor()
158
+ try {
159
+ expect(addEventListener).not.toHaveBeenCalled()
160
+
161
+ soloEditor.store.put([createPresence(soloEditor)])
162
+ expect(addEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
163
+ capture: true,
164
+ })
165
+
166
+ soloEditor.store.remove([createPresence(soloEditor).id])
167
+ expect(removeEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
168
+ capture: true,
169
+ })
170
+ } finally {
171
+ soloEditor.dispose()
172
+ addEventListener.mockRestore()
173
+ removeEventListener.mockRestore()
174
+ }
175
+ })
176
+
177
+ it('attaches no input listeners when construction fails before mount', () => {
178
+ class DuplicateTool extends StateNode {
179
+ static override id = 'duplicate-tool'
180
+ }
181
+
182
+ const addEventListener = vi.spyOn(window, 'addEventListener')
183
+ try {
184
+ const store = createTLStore({})
185
+ store.ensureStoreIsUsable()
186
+ expect(
187
+ () =>
188
+ new Editor({
189
+ store,
190
+ bindingUtils: [],
191
+ shapeUtils: [],
192
+ getContainer: () => document.createElement('div'),
193
+ // The duplicate id makes the constructor throw after the inputs
194
+ // manager is built, so the editor is never disposed.
195
+ tools: [DuplicateTool, DuplicateTool],
196
+ })
197
+ ).toThrow()
198
+
199
+ expect(addEventListener).not.toHaveBeenCalled()
200
+ } finally {
201
+ addEventListener.mockRestore()
202
+ }
203
+ })
204
+
205
+ it('does not stamp on pointer moves with no DOM input, e.g. following a user', () => {
206
+ editor.store.put([createPresence(editor)])
207
+
208
+ editor.inputs.markActivity()
209
+ const stamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
210
+
211
+ vi.advanceTimersByTime(5000)
212
+ editor.inputs.updateFromEvent({
213
+ type: 'pointer',
214
+ name: 'pointer_move',
215
+ point: { x: 100, y: 50 },
216
+ pointerId: 1,
217
+ button: 0,
218
+ isPen: false,
219
+ target: 'canvas',
220
+ shiftKey: false,
221
+ altKey: false,
222
+ ctrlKey: false,
223
+ metaKey: false,
224
+ accelKey: false,
225
+ })
226
+
227
+ // The pointer position updates, but the activity clock doesn't.
228
+ expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
229
+ x: 100,
230
+ y: 50,
231
+ lastActivityTimestamp: stamp,
232
+ })
233
+ })
234
+ })
235
+
40
236
  it('stops updating pointer velocity after dispose', () => {
41
237
  const point = editor.inputs.getCurrentScreenPoint()
42
238
  point.x = 0
@@ -1,8 +1,7 @@
1
- import { atom, computed, unsafe__withoutCapture } from '@tldraw/state'
1
+ import { atom, computed, react, unsafe__withoutCapture } from '@tldraw/state'
2
2
  import { AtomSet } from '@tldraw/store'
3
3
  import { TLINSTANCE_ID, TLPOINTER_ID } from '@tldraw/tlschema'
4
- import { bind } from '@tldraw/utils'
5
- import { INTERNAL_POINTER_IDS } from '../../../constants'
4
+ import { bind, throttle } from '@tldraw/utils'
6
5
  import { Vec } from '../../../primitives/Vec'
7
6
  import { isAccelKey } from '../../../utils/keyboard'
8
7
  import type { Editor } from '../../Editor'
@@ -12,11 +11,85 @@ import { EditorManager } from '../EditorManager'
12
11
  const POINTER_VELOCITY_REFERENCE_INTERVAL_MS = 16
13
12
  const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
14
13
 
14
+ // How often `markActivity` is allowed to stamp the activity timestamp. Each
15
+ // stamp re-broadcasts presence, so this must be long enough to keep input
16
+ // bursts (e.g. typing, held keys) from flooding the network. It must also stay
17
+ // comfortably shorter than `collaboratorIdleTimeoutMs` so a continuously-active
18
+ // peer never flickers to idle between stamps; the throttle window is capped
19
+ // below that option when it's configured shorter than the default, but never
20
+ // below the floor, so a degenerate idle timeout can't disable throttling.
21
+ const ACTIVITY_TIMESTAMP_THROTTLE_MS = 1000
22
+ const ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS = 100
23
+
24
+ // DOM events that count as presence activity. `beforeinput` covers IME and
25
+ // mobile keyboards (e.g. gesture typing), which often don't fire per-character
26
+ // keydown events. `gesturestart`/`gesturechange` are Safari's proprietary
27
+ // trackpad-pinch events, which produce neither pointer nor wheel events.
28
+ const ACTIVITY_EVENTS = [
29
+ 'pointerdown',
30
+ 'pointermove',
31
+ 'pointerup',
32
+ 'keydown',
33
+ 'beforeinput',
34
+ 'wheel',
35
+ 'gesturestart',
36
+ 'gesturechange',
37
+ ]
38
+
15
39
  /** @public */
16
40
  export class InputsManager extends EditorManager {
17
41
  constructor(editor: Editor) {
18
42
  super(editor)
19
43
  this.addEditorEvent('frame', this._onFrame)
44
+
45
+ // Track input only while mounted and sharing with someone: a solo user
46
+ // pays for no listeners at all, and the window is resolved on attach
47
+ // rather than up front, when the container is guaranteed to exist.
48
+ //
49
+ // The mount check must come first. This runs eagerly during the editor's
50
+ // constructor, where `editor.collaborators` isn't assigned yet, so reading
51
+ // collaborators here would throw; a never-mounted editor short-circuits
52
+ // before that, and re-runs once mount flips the atom.
53
+ this.register(
54
+ react('presence activity listeners', () => {
55
+ if (this.editor.getIsMounted() && this._getHasCollaborators()) {
56
+ this._attachActivityListeners()
57
+ } else {
58
+ this._detachActivityListeners()
59
+ }
60
+ })
61
+ )
62
+ this.register(() => {
63
+ this._detachActivityListeners()
64
+ this._throttledActivityStamp.cancel()
65
+ })
66
+ }
67
+
68
+ private _detachActivityListenersFn: null | (() => void) = null
69
+
70
+ /**
71
+ * User input anywhere in the editor's tab counts as activity for collaborator presence —
72
+ * presence means "this person is here", whether or not the input lands on the editor itself
73
+ * (e.g. typing in a sidebar next to the canvas). Listen in the capture phase so input that
74
+ * other handlers swallow still counts.
75
+ */
76
+ private _attachActivityListeners() {
77
+ if (this._detachActivityListenersFn) return
78
+
79
+ const win = this.editor.getContainerWindow()
80
+ for (const name of ACTIVITY_EVENTS) {
81
+ win.addEventListener(name, this.markActivity, { capture: true })
82
+ }
83
+ this._detachActivityListenersFn = () => {
84
+ for (const name of ACTIVITY_EVENTS) {
85
+ win.removeEventListener(name, this.markActivity, { capture: true })
86
+ }
87
+ }
88
+ }
89
+
90
+ private _detachActivityListeners() {
91
+ this._detachActivityListenersFn?.()
92
+ this._detachActivityListenersFn = null
20
93
  }
21
94
 
22
95
  @bind
@@ -462,6 +535,53 @@ export class InputsManager extends EditorManager {
462
535
  return this.editor.getCollaborators().length > 0 // could we do this more efficiently?
463
536
  }
464
537
 
538
+ private readonly _throttledActivityStamp = throttle(
539
+ () => {
540
+ const pointer = this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
541
+ if (!pointer) return
542
+
543
+ this.editor.run(
544
+ () => {
545
+ this.editor.store.put([{ ...pointer, lastActivityTimestamp: Date.now() }])
546
+ },
547
+ { history: 'ignore' }
548
+ )
549
+ },
550
+ // Stay comfortably below the idle timeout so a continuously-active peer
551
+ // never flickers to idle between throttled stamps.
552
+ Math.max(
553
+ ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS,
554
+ Math.min(ACTIVITY_TIMESTAMP_THROTTLE_MS, this.editor.options.collaboratorIdleTimeoutMs / 3)
555
+ ),
556
+ { trailing: false }
557
+ )
558
+
559
+ /**
560
+ * Mark the current user as active for collaborator presence. User input anywhere in the
561
+ * editor's tab counts as activity automatically; call this to make input that doesn't produce
562
+ * DOM events count too, so peers don't classify this user as idle or inactive while they're
563
+ * still interacting. Calls are throttled on the leading edge, so it's safe to call from
564
+ * high-frequency input events.
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * // Gamepads are polled rather than event-driven, so stamp activity from the poll
569
+ * editor.timers.setInterval(() => {
570
+ * const isPressed = navigator
571
+ * .getGamepads()
572
+ * .some((pad) => pad?.buttons.some((button) => button.pressed))
573
+ * if (isPressed) editor.inputs.markActivity()
574
+ * }, 50)
575
+ * ```
576
+ */
577
+ @bind
578
+ markActivity() {
579
+ // Check for collaborators before consuming the throttle window, so input
580
+ // while alone doesn't delay the first stamp after a peer joins.
581
+ if (!this._getHasCollaborators()) return
582
+ this._throttledActivityStamp()
583
+ }
584
+
465
585
  /**
466
586
  * The previous point used for velocity calculation (updated each tick, not each pointer event).
467
587
  * @internal
@@ -553,13 +673,12 @@ export class InputsManager extends EditorManager {
553
673
  typeName: 'pointer',
554
674
  x: pagePoint.x,
555
675
  y: pagePoint.y,
676
+ // The activity timestamp is stamped by the container's input
677
+ // listeners (see `markActivity`), not by pointer position updates:
678
+ // synthetic moves, like the camera following another user, don't
679
+ // count as activity.
556
680
  lastActivityTimestamp:
557
- // If our pointer moved only because we're following some other user, then don't
558
- // update our last activity timestamp; otherwise, update it to the current timestamp.
559
- info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
560
- ? (this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
561
- ?.lastActivityTimestamp ?? Date.now())
562
- : Date.now(),
681
+ this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ?? 0,
563
682
  meta: {},
564
683
  },
565
684
  ])
@@ -1,4 +1,4 @@
1
- import { TLScribble, VecModel } from '@tldraw/tlschema'
1
+ import { TLPageId, TLScribble, VecModel } from '@tldraw/tlschema'
2
2
  import { uniqueId } from '@tldraw/utils'
3
3
  import { Vec } from '../../../primitives/Vec'
4
4
  import type { Editor } from '../../Editor'
@@ -47,6 +47,7 @@ export interface ScribbleSessionOptions {
47
47
  // Internal session state (not exported)
48
48
  interface Session {
49
49
  id: string
50
+ pageId: TLPageId
50
51
  items: ScribbleItem[]
51
52
  state: 'active' | 'stopping' | 'complete'
52
53
  options: Required<Omit<ScribbleSessionOptions, 'id'>>
@@ -75,6 +76,7 @@ export class ScribbleManager {
75
76
  const id = options.id ?? uniqueId()
76
77
  const session: Session = {
77
78
  id,
79
+ pageId: this.editor.getCurrentPageId(),
78
80
  items: [],
79
81
  state: 'active',
80
82
  options: {
@@ -357,6 +359,15 @@ export class ScribbleManager {
357
359
  if (this.sessions.size === 0 && currentScribbles.length === 0) return
358
360
 
359
361
  this.editor.run(() => {
362
+ // Drop sessions from other pages, otherwise a still-fading laser trail
363
+ // keeps playing out over the page the user switched to (#10449).
364
+ const currentPageId = this.editor.getCurrentPageId()
365
+ for (const session of this.sessions.values()) {
366
+ if (session.pageId !== currentPageId) {
367
+ this.clearSession(session.id)
368
+ }
369
+ }
370
+
360
371
  // Tick all sessions
361
372
  for (const session of this.sessions.values()) {
362
373
  this.tickSession(session, elapsed)
@@ -206,12 +206,10 @@ export class BoundsSnaps {
206
206
  const { editor } = this
207
207
  return editor.store.createComputedCache<BoundsSnapPoint[], TLShape>('snapPoints', (shape) => {
208
208
  const pageTransform = editor.getShapePageTransform(shape.id)
209
- if (!pageTransform) return undefined
210
209
  const boundsSnapGeometry = editor.getShapeUtil(shape).getBoundsSnapGeometry(shape)
211
210
  const snapPoints =
212
211
  boundsSnapGeometry.points ?? editor.getShapeGeometry(shape).bounds.cornersAndCenter
213
212
 
214
- if (!pageTransform || !snapPoints) return undefined
215
213
  return snapPoints.map((point, i) => {
216
214
  const { x, y } = Mat.applyToPoint(pageTransform, point)
217
215
  return { x, y, id: `${shape.id}:${i}` }
@@ -1,5 +1,5 @@
1
1
  import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
- import { objectMapKeys } from '@tldraw/utils'
2
+ import { iterateGraphemes, objectMapKeys } from '@tldraw/utils'
3
3
  import type { Editor } from '../../Editor'
4
4
  import { EditorManager } from '../EditorManager'
5
5
 
@@ -94,10 +94,6 @@ export interface TLMeasureTextSpanOpts {
94
94
 
95
95
  const spaceCharacterRegex = /\s/
96
96
 
97
- // Iterate by grapheme cluster (e.g. emoji sequences, flags, accented letters)
98
- // rather than by code point, so a single glyph is measured as one unit.
99
- const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
100
-
101
97
  const initialDefaultStyles = Object.freeze({
102
98
  'overflow-wrap': 'break-word',
103
99
  'word-break': 'auto',
@@ -309,7 +305,7 @@ export class TextManager extends EditorManager {
309
305
  for (const childNode of element.childNodes) {
310
306
  if (childNode.nodeType !== Node.TEXT_NODE) continue
311
307
 
312
- for (const { segment } of graphemeSegmenter.segment(childNode.textContent ?? '')) {
308
+ for (const segment of iterateGraphemes(childNode.textContent ?? '')) {
313
309
  // place the range around the grapheme we're interested in
314
310
  range.setStart(textNode, idx)
315
311
  range.setEnd(textNode, idx + segment.length)