@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist-cjs/index.d.ts +114 -47
  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/editor/Editor.js +181 -294
  17. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  18. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  19. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  23. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  25. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  27. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  29. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  31. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  33. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  35. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  37. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  39. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  41. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  42. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  43. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  44. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  45. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  46. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  47. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  48. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  49. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  50. package/dist-cjs/lib/globals/editors.js +57 -0
  51. package/dist-cjs/lib/globals/editors.js.map +7 -0
  52. package/dist-cjs/lib/globals/menus.js +9 -4
  53. package/dist-cjs/lib/globals/menus.js.map +2 -2
  54. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  55. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  56. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  57. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  58. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  59. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  60. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  61. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  63. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  64. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  65. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  66. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  67. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  68. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  69. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  70. package/dist-cjs/lib/primitives/Box.js +2 -1
  71. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  72. package/dist-cjs/lib/primitives/Vec.js +4 -3
  73. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  74. package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
  75. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  76. package/dist-cjs/lib/primitives/utils.js +2 -1
  77. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  78. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  79. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  80. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  81. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  82. package/dist-cjs/lib/utils/rotation.js +1 -9
  83. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  84. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  85. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  86. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  87. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  88. package/dist-cjs/version.js +3 -3
  89. package/dist-cjs/version.js.map +1 -1
  90. package/dist-esm/index.d.mts +114 -47
  91. package/dist-esm/index.mjs +9 -2
  92. package/dist-esm/index.mjs.map +2 -2
  93. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  94. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  95. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  96. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  97. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  98. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  99. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  100. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  101. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  102. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  103. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  104. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  105. package/dist-esm/lib/editor/Editor.mjs +189 -295
  106. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  107. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  108. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  109. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  110. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  111. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  112. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  113. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  114. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  115. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  116. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  117. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  118. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  119. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  120. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  121. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  122. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  123. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  124. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  126. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  128. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  130. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  131. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  132. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  133. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  134. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  135. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  136. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  137. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  138. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  139. package/dist-esm/lib/globals/editors.mjs +37 -0
  140. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  141. package/dist-esm/lib/globals/menus.mjs +9 -4
  142. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  143. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  144. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  145. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  146. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  147. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  148. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  149. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  150. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  151. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  152. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  153. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  154. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  155. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  156. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  157. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  158. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  159. package/dist-esm/lib/primitives/Box.mjs +2 -1
  160. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  161. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  162. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  163. package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
  164. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  165. package/dist-esm/lib/primitives/utils.mjs +2 -1
  166. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  167. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  168. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  169. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  170. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  171. package/dist-esm/lib/utils/rotation.mjs +1 -9
  172. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  173. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  174. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  175. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  176. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  177. package/dist-esm/version.mjs +3 -3
  178. package/dist-esm/version.mjs.map +1 -1
  179. package/package.json +13 -7
  180. package/src/index.ts +6 -1
  181. package/src/lib/TldrawEditor.tsx +16 -4
  182. package/src/lib/components/MenuClickCapture.tsx +19 -18
  183. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  184. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  185. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  186. package/src/lib/config/TLUserPreferences.ts +8 -4
  187. package/src/lib/config/createTLStore.ts +1 -1
  188. package/src/lib/editor/Editor.ts +266 -383
  189. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  190. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  191. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  192. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  193. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  194. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  195. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  196. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  197. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  198. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  199. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  200. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  201. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  202. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  203. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  204. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  205. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  206. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  207. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  208. package/src/lib/editor/tools/StateNode.ts +12 -2
  209. package/src/lib/globals/editors.test.ts +76 -0
  210. package/src/lib/globals/editors.ts +53 -0
  211. package/src/lib/globals/menus.test.ts +33 -0
  212. package/src/lib/globals/menus.ts +11 -4
  213. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  214. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  215. package/src/lib/hooks/useLocalStore.ts +16 -8
  216. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  217. package/src/lib/hooks/usePeerIds.ts +2 -1
  218. package/src/lib/license/LicenseManager.test.ts +160 -0
  219. package/src/lib/license/LicenseManager.ts +13 -4
  220. package/src/lib/license/LicenseProvider.tsx +24 -3
  221. package/src/lib/license/useLicenseManagerState.ts +8 -6
  222. package/src/lib/primitives/Box.test.ts +9 -1
  223. package/src/lib/primitives/Box.ts +3 -1
  224. package/src/lib/primitives/Vec.test.ts +10 -0
  225. package/src/lib/primitives/Vec.ts +4 -4
  226. package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
  227. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
  228. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  229. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  230. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  231. package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
  232. package/src/lib/primitives/geometry/Group2d.ts +63 -27
  233. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  234. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  235. package/src/lib/primitives/utils.test.ts +10 -1
  236. package/src/lib/primitives/utils.ts +4 -1
  237. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  238. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  239. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  240. package/src/lib/utils/deepLinks.test.ts +15 -0
  241. package/src/lib/utils/deepLinks.ts +7 -1
  242. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  243. package/src/lib/utils/normalizeWheel.ts +6 -0
  244. package/src/lib/utils/rotation.ts +4 -12
  245. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  246. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  247. package/src/lib/utils/sync/alerts.ts +1 -1
  248. package/src/version.ts +3 -3
  249. 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
  /**
@@ -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)
@@ -142,6 +142,19 @@ describe('TickManager', () => {
142
142
  expect(mockEmit).toHaveBeenCalledWith('frame', 0)
143
143
  expect(mockEmit).toHaveBeenCalledWith('tick', 0)
144
144
  })
145
+
146
+ it('should schedule the next frame even when a listener throws', () => {
147
+ const staleCancel = vi.fn()
148
+ tickManager.cancelRaf = staleCancel
149
+ mockEmit.mockImplementation(() => {
150
+ throw new Error('listener blew up')
151
+ })
152
+
153
+ // the error still reaches the caller, but the loop lives on
154
+ expect(() => tickManager.tick()).toThrow('listener blew up')
155
+ expect(mockRequestAnimationFrame).toHaveBeenCalled()
156
+ expect(tickManager.cancelRaf).not.toBe(staleCancel)
157
+ })
145
158
  })
146
159
 
147
160
  describe('dispose method', () => {
@@ -41,9 +41,15 @@ export class TickManager extends EditorManager {
41
41
  const elapsed = now - this.now
42
42
  this.now = now
43
43
 
44
- this.editor.emit('frame', elapsed)
45
- this.editor.emit('tick', elapsed)
46
- this.cancelRaf = throttleToNextFrame(this.tick)
44
+ // Schedule the next frame even if a listener throws. Otherwise a single throwing listener
45
+ // ends the loop for good, which silently stops every frame-driven behavior — camera
46
+ // animations, edge scrolling, pointer velocity, following — instead of failing once.
47
+ try {
48
+ this.editor.emit('frame', elapsed)
49
+ this.editor.emit('tick', elapsed)
50
+ } finally {
51
+ this.cancelRaf = throttleToNextFrame(this.tick)
52
+ }
47
53
  }
48
54
 
49
55
  dispose() {