@tldraw/editor 4.6.0-next.fe1474dc57d8 → 5.0.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 (207) hide show
  1. package/dist-cjs/index.d.ts +412 -179
  2. package/dist-cjs/index.js +12 -23
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +3 -0
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +180 -0
  7. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +7 -0
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +44 -249
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +3 -3
  10. package/dist-cjs/lib/editor/Editor.js +78 -28
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +98 -0
  13. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +7 -0
  14. package/dist-cjs/lib/editor/managers/ThemeManager/defaultThemes.js +14 -0
  15. package/dist-cjs/lib/editor/managers/ThemeManager/defaultThemes.js.map +2 -2
  16. package/dist-cjs/lib/editor/overlays/OverlayManager.js +154 -0
  17. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/overlays/OverlayUtil.js +92 -0
  19. package/dist-cjs/lib/editor/overlays/OverlayUtil.js.map +7 -0
  20. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +161 -0
  21. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +7 -0
  22. package/dist-cjs/lib/editor/overlays/getOverlayDisplayValues.js +39 -0
  23. package/dist-cjs/lib/editor/overlays/getOverlayDisplayValues.js.map +7 -0
  24. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
  25. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +25 -23
  27. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  28. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +32 -2
  29. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  30. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  31. package/dist-cjs/lib/exports/fetchCache.js +1 -1
  32. package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
  33. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useCanvasEvents.js +3 -3
  35. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  36. package/dist-cjs/lib/hooks/useEditorComponents.js +0 -28
  37. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  38. package/dist-cjs/lib/hooks/usePeerIds.js +1 -36
  39. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  40. package/dist-cjs/lib/hooks/useShapeCulling.js +2 -1
  41. package/dist-cjs/lib/hooks/useShapeCulling.js.map +2 -2
  42. package/dist-cjs/lib/options.js +0 -1
  43. package/dist-cjs/lib/options.js.map +2 -2
  44. package/dist-cjs/lib/utils/reparenting.js +20 -7
  45. package/dist-cjs/lib/utils/reparenting.js.map +2 -2
  46. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +3 -0
  47. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  48. package/dist-cjs/version.js +4 -4
  49. package/dist-cjs/version.js.map +1 -1
  50. package/dist-esm/index.d.mts +412 -179
  51. package/dist-esm/index.mjs +19 -41
  52. package/dist-esm/index.mjs.map +2 -2
  53. package/dist-esm/lib/TldrawEditor.mjs +3 -0
  54. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +160 -0
  56. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +7 -0
  57. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +45 -250
  58. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +3 -3
  59. package/dist-esm/lib/editor/Editor.mjs +78 -29
  60. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  61. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +83 -0
  62. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +7 -0
  63. package/dist-esm/lib/editor/managers/ThemeManager/defaultThemes.mjs +14 -0
  64. package/dist-esm/lib/editor/managers/ThemeManager/defaultThemes.mjs.map +2 -2
  65. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +136 -0
  66. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +7 -0
  67. package/dist-esm/lib/editor/overlays/OverlayUtil.mjs +72 -0
  68. package/dist-esm/lib/editor/overlays/OverlayUtil.mjs.map +7 -0
  69. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +141 -0
  70. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +7 -0
  71. package/dist-esm/lib/editor/overlays/getOverlayDisplayValues.mjs +19 -0
  72. package/dist-esm/lib/editor/overlays/getOverlayDisplayValues.mjs.map +7 -0
  73. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
  74. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  75. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +25 -23
  76. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  77. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +32 -2
  78. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  79. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  80. package/dist-esm/lib/exports/fetchCache.mjs +2 -2
  81. package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  83. package/dist-esm/lib/hooks/useCanvasEvents.mjs +3 -3
  84. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  85. package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -28
  86. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  87. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -40
  88. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  89. package/dist-esm/lib/hooks/useShapeCulling.mjs +2 -1
  90. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +2 -2
  91. package/dist-esm/lib/options.mjs +0 -1
  92. package/dist-esm/lib/options.mjs.map +2 -2
  93. package/dist-esm/lib/utils/reparenting.mjs +20 -7
  94. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  95. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +3 -0
  96. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  97. package/dist-esm/version.mjs +4 -4
  98. package/dist-esm/version.mjs.map +1 -1
  99. package/editor.css +4 -239
  100. package/package.json +7 -7
  101. package/src/index.ts +17 -39
  102. package/src/lib/TldrawEditor.tsx +9 -0
  103. package/src/lib/components/default-components/CanvasOverlays.tsx +208 -0
  104. package/src/lib/components/default-components/DefaultCanvas.tsx +49 -324
  105. package/src/lib/editor/Editor.test.ts +3 -1
  106. package/src/lib/editor/Editor.ts +80 -24
  107. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +98 -0
  108. package/src/lib/editor/managers/ThemeManager/defaultThemes.ts +14 -0
  109. package/src/lib/editor/overlays/OverlayManager.ts +183 -0
  110. package/src/lib/editor/overlays/OverlayUtil.ts +143 -0
  111. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +216 -0
  112. package/src/lib/editor/overlays/getOverlayDisplayValues.ts +51 -0
  113. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +9 -2
  114. package/src/lib/editor/shapes/ShapeUtil.ts +34 -26
  115. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +40 -3
  116. package/src/lib/editor/types/event-types.ts +2 -0
  117. package/src/lib/exports/fetchCache.ts +2 -4
  118. package/src/lib/exports/getSvgJsx.test.ts +3 -1
  119. package/src/lib/hooks/EditorComponentsContext.tsx +0 -27
  120. package/src/lib/hooks/useCanvasEvents.ts +13 -8
  121. package/src/lib/hooks/useEditorComponents.tsx +0 -28
  122. package/src/lib/hooks/usePeerIds.ts +6 -55
  123. package/src/lib/hooks/useShapeCulling.tsx +3 -1
  124. package/src/lib/options.ts +0 -7
  125. package/src/lib/utils/reparenting.ts +22 -9
  126. package/src/lib/utils/sync/TLLocalSyncClient.ts +3 -0
  127. package/src/version.ts +4 -4
  128. package/dist-cjs/lib/components/GeometryDebuggingView.js +0 -115
  129. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +0 -7
  130. package/dist-cjs/lib/components/LiveCollaborators.js +0 -152
  131. package/dist-cjs/lib/components/LiveCollaborators.js.map +0 -7
  132. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +0 -234
  133. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +0 -7
  134. package/dist-cjs/lib/components/default-components/DefaultBrush.js +0 -38
  135. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +0 -7
  136. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +0 -71
  137. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +0 -7
  138. package/dist-cjs/lib/components/default-components/DefaultCursor.js +0 -59
  139. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +0 -7
  140. package/dist-cjs/lib/components/default-components/DefaultHandle.js +0 -56
  141. package/dist-cjs/lib/components/default-components/DefaultHandle.js.map +0 -7
  142. package/dist-cjs/lib/components/default-components/DefaultHandles.js +0 -28
  143. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +0 -7
  144. package/dist-cjs/lib/components/default-components/DefaultScribble.js +0 -51
  145. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +0 -7
  146. package/dist-cjs/lib/components/default-components/DefaultSelectionForeground.js +0 -69
  147. package/dist-cjs/lib/components/default-components/DefaultSelectionForeground.js.map +0 -7
  148. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +0 -107
  149. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +0 -7
  150. package/dist-cjs/lib/components/default-components/DefaultShapeIndicatorErrorFallback.js +0 -28
  151. package/dist-cjs/lib/components/default-components/DefaultShapeIndicatorErrorFallback.js.map +0 -7
  152. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +0 -102
  153. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +0 -7
  154. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +0 -170
  155. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +0 -7
  156. package/dist-cjs/lib/hooks/useHandleEvents.js +0 -100
  157. package/dist-cjs/lib/hooks/useHandleEvents.js.map +0 -7
  158. package/dist-cjs/lib/hooks/useSelectionEvents.js +0 -98
  159. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +0 -7
  160. package/dist-esm/lib/components/GeometryDebuggingView.mjs +0 -95
  161. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +0 -7
  162. package/dist-esm/lib/components/LiveCollaborators.mjs +0 -135
  163. package/dist-esm/lib/components/LiveCollaborators.mjs.map +0 -7
  164. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +0 -214
  165. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +0 -7
  166. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +0 -18
  167. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +0 -7
  168. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +0 -41
  169. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +0 -7
  170. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +0 -29
  171. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +0 -7
  172. package/dist-esm/lib/components/default-components/DefaultHandle.mjs +0 -26
  173. package/dist-esm/lib/components/default-components/DefaultHandle.mjs.map +0 -7
  174. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +0 -8
  175. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +0 -7
  176. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +0 -21
  177. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +0 -7
  178. package/dist-esm/lib/components/default-components/DefaultSelectionForeground.mjs +0 -39
  179. package/dist-esm/lib/components/default-components/DefaultSelectionForeground.mjs.map +0 -7
  180. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +0 -77
  181. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +0 -7
  182. package/dist-esm/lib/components/default-components/DefaultShapeIndicatorErrorFallback.mjs +0 -8
  183. package/dist-esm/lib/components/default-components/DefaultShapeIndicatorErrorFallback.mjs.map +0 -7
  184. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +0 -82
  185. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +0 -7
  186. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +0 -142
  187. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +0 -7
  188. package/dist-esm/lib/hooks/useHandleEvents.mjs +0 -70
  189. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +0 -7
  190. package/dist-esm/lib/hooks/useSelectionEvents.mjs +0 -78
  191. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +0 -7
  192. package/src/lib/components/GeometryDebuggingView.tsx +0 -108
  193. package/src/lib/components/LiveCollaborators.tsx +0 -180
  194. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +0 -300
  195. package/src/lib/components/default-components/DefaultBrush.tsx +0 -35
  196. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +0 -52
  197. package/src/lib/components/default-components/DefaultCursor.tsx +0 -59
  198. package/src/lib/components/default-components/DefaultHandle.tsx +0 -42
  199. package/src/lib/components/default-components/DefaultHandles.tsx +0 -15
  200. package/src/lib/components/default-components/DefaultScribble.tsx +0 -31
  201. package/src/lib/components/default-components/DefaultSelectionForeground.tsx +0 -50
  202. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +0 -104
  203. package/src/lib/components/default-components/DefaultShapeIndicatorErrorFallback.tsx +0 -9
  204. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +0 -118
  205. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +0 -174
  206. package/src/lib/hooks/useHandleEvents.ts +0 -88
  207. package/src/lib/hooks/useSelectionEvents.ts +0 -97
@@ -89,7 +89,6 @@ import {
89
89
  hasOwnProperty,
90
90
  last,
91
91
  lerp,
92
- maxBy,
93
92
  minBy,
94
93
  sortById,
95
94
  sortByIndex,
@@ -152,6 +151,7 @@ import { notVisibleShapes } from './derivations/notVisibleShapes'
152
151
  import { parentsToChildren } from './derivations/parentsToChildren'
153
152
  import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
154
153
  import { ClickManager } from './managers/ClickManager/ClickManager'
154
+ import { CollaboratorsManager } from './managers/CollaboratorsManager/CollaboratorsManager'
155
155
  import { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManager'
156
156
  import { FocusManager } from './managers/FocusManager/FocusManager'
157
157
  import { FontManager } from './managers/FontManager/FontManager'
@@ -165,6 +165,8 @@ import { TextManager } from './managers/TextManager/TextManager'
165
165
  import { ThemeManager, resolveThemes } from './managers/ThemeManager/ThemeManager'
166
166
  import { TickManager } from './managers/TickManager/TickManager'
167
167
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
168
+ import { OverlayManager } from './overlays/OverlayManager'
169
+ import { TLAnyOverlayUtilConstructor } from './overlays/OverlayUtil'
168
170
  import {
169
171
  ShapeUtil,
170
172
  TLEditStartInfo,
@@ -224,6 +226,11 @@ export interface TLEditorOptions {
224
226
  * An array of asset utils to use in the editor. These will be used to handle asset-type-specific behavior.
225
227
  */
226
228
  assetUtils?: readonly TLAnyAssetUtilConstructor[]
229
+ /**
230
+ * An array of overlay utils to use in the editor. These define canvas overlay UI elements
231
+ * like selection handles, rotation corners, shape handles, etc.
232
+ */
233
+ overlayUtils?: readonly TLAnyOverlayUtilConstructor[]
227
234
  /**
228
235
  * An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
229
236
  */
@@ -330,6 +337,7 @@ export class Editor extends EventEmitter<TLEventMap> {
330
337
  shapeUtils,
331
338
  bindingUtils,
332
339
  assetUtils: assetUtilConstructors,
340
+ overlayUtils: overlayUtilConstructors,
333
341
  tools,
334
342
  getContainer,
335
343
  // needs to be here for backwards compatibility with TldrawEditor
@@ -410,6 +418,7 @@ export class Editor extends EventEmitter<TLEventMap> {
410
418
  this.inputs = new InputsManager(this)
411
419
  this.performance = new PerformanceManager(this)
412
420
  this.disposables.add(() => this.performance.dispose())
421
+ this.collaborators = new CollaboratorsManager(this)
413
422
 
414
423
  class NewRoot extends RootState {
415
424
  static override initial = initialState ?? ''
@@ -489,6 +498,15 @@ export class Editor extends EventEmitter<TLEventMap> {
489
498
 
490
499
  this.scribbles = new ScribbleManager(this)
491
500
 
501
+ // Overlay utils
502
+ this.overlays = new OverlayManager(this)
503
+ if (overlayUtilConstructors) {
504
+ for (const Util of overlayUtilConstructors) {
505
+ const util = new Util(this)
506
+ this.overlays.registerUtil(util)
507
+ }
508
+ }
509
+
492
510
  // Cleanup
493
511
 
494
512
  const cleanupInstancePageState = (
@@ -1033,6 +1051,13 @@ export class Editor extends EventEmitter<TLEventMap> {
1033
1051
  */
1034
1052
  readonly timers = tltime.forContext(this.contextId)
1035
1053
 
1054
+ /**
1055
+ * A manager for remote peer collaborators connected to this editor.
1056
+ *
1057
+ * @public
1058
+ */
1059
+ readonly collaborators: CollaboratorsManager
1060
+
1036
1061
  /**
1037
1062
  * A manager for the user and their preferences.
1038
1063
  *
@@ -1068,6 +1093,13 @@ export class Editor extends EventEmitter<TLEventMap> {
1068
1093
  */
1069
1094
  readonly scribbles: ScribbleManager
1070
1095
 
1096
+ /**
1097
+ * A manager for canvas overlay UI elements (selection handles, shape handles, etc.).
1098
+ *
1099
+ * @public
1100
+ */
1101
+ readonly overlays: OverlayManager
1102
+
1071
1103
  /**
1072
1104
  * A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details.
1073
1105
  *
@@ -1912,11 +1944,23 @@ export class Editor extends EventEmitter<TLEventMap> {
1912
1944
  /**
1913
1945
  * Set the cursor.
1914
1946
  *
1947
+ * No-op when the partial wouldn't change the current cursor — `setCursor`
1948
+ * is called from pointer-move hot paths (see `updateHoveredOverlayId`,
1949
+ * various tool states) and skipping redundant writes avoids needlessly
1950
+ * dirtying instance state.
1951
+ *
1915
1952
  * @param cursor - The cursor to set.
1916
1953
  * @public
1917
1954
  */
1918
1955
  setCursor(cursor: Partial<TLCursor>) {
1919
- this.updateInstanceState({ cursor: { ...this.getInstanceState().cursor, ...cursor } })
1956
+ const current = this.getInstanceState().cursor
1957
+ if (
1958
+ (cursor.type === undefined || cursor.type === current.type) &&
1959
+ (cursor.rotation === undefined || cursor.rotation === current.rotation)
1960
+ ) {
1961
+ return this
1962
+ }
1963
+ this.updateInstanceState({ cursor: { ...current, ...cursor } })
1920
1964
  return this
1921
1965
  }
1922
1966
 
@@ -4222,43 +4266,55 @@ export class Editor extends EventEmitter<TLEventMap> {
4222
4266
  }
4223
4267
  // Collaborators
4224
4268
 
4225
- @computed
4226
- private _getCollaboratorsQuery() {
4227
- return this.store.query.records('instance_presence', () => ({
4228
- userId: { neq: this.user.getId() },
4229
- }))
4230
- }
4231
-
4232
4269
  /**
4233
4270
  * Returns a list of presence records for all peer collaborators.
4234
4271
  * This will return the latest presence record for each connected user.
4235
4272
  *
4273
+ * Convenience wrapper for {@link CollaboratorsManager.getCollaborators}.
4274
+ *
4236
4275
  * @public
4237
4276
  */
4238
- @computed
4239
4277
  getCollaborators() {
4240
- const allPresenceRecords = this._getCollaboratorsQuery().get()
4241
- if (!allPresenceRecords.length) return EMPTY_ARRAY
4242
- const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort()
4243
- return userIds.map((id) => {
4244
- const latestPresence = maxBy(
4245
- allPresenceRecords.filter((c) => c.userId === id),
4246
- (p) => p.lastActivityTimestamp ?? 0
4247
- )
4248
- return latestPresence!
4249
- })
4278
+ return this.collaborators.getCollaborators()
4250
4279
  }
4251
4280
 
4252
4281
  /**
4253
4282
  * Returns a list of presence records for all peer collaborators on the current page.
4254
4283
  * This will return the latest presence record for each connected user.
4255
4284
  *
4285
+ * Convenience wrapper for {@link CollaboratorsManager.getCollaboratorsOnCurrentPage}.
4286
+ *
4256
4287
  * @public
4257
4288
  */
4258
- @computed
4259
4289
  getCollaboratorsOnCurrentPage() {
4260
- const currentPageId = this.getCurrentPageId()
4261
- return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
4290
+ return this.collaborators.getCollaboratorsOnCurrentPage()
4291
+ }
4292
+
4293
+ /**
4294
+ * Returns a list of presence records for peer collaborators who should currently be
4295
+ * shown in the UI. Filters {@link Editor.getCollaborators} by activity state
4296
+ * (active / idle / inactive) and visibility rules such as following and highlighted
4297
+ * users. Re-evaluates on the collaborator visibility clock, so callers don't need to
4298
+ * drive their own activity timer.
4299
+ *
4300
+ * Convenience wrapper for {@link CollaboratorsManager.getVisibleCollaborators}.
4301
+ *
4302
+ * @public
4303
+ */
4304
+ getVisibleCollaborators() {
4305
+ return this.collaborators.getVisibleCollaborators()
4306
+ }
4307
+
4308
+ /**
4309
+ * Returns a list of presence records for peer collaborators who should currently be
4310
+ * shown in the UI, filtered to those on the current page.
4311
+ *
4312
+ * Convenience wrapper for {@link CollaboratorsManager.getVisibleCollaboratorsOnCurrentPage}.
4313
+ *
4314
+ * @public
4315
+ */
4316
+ getVisibleCollaboratorsOnCurrentPage() {
4317
+ return this.collaborators.getVisibleCollaboratorsOnCurrentPage()
4262
4318
  }
4263
4319
 
4264
4320
  // Attribution
@@ -5112,7 +5168,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5112
5168
  const zoomStepFunction = (zoom: number) => Math.pow(2, Math.ceil(Math.log2(zoom)))
5113
5169
  const steppedScreenScale = zoomStepFunction(screenScale)
5114
5170
  const networkEffectiveType: string | null =
5115
- 'connection' in navigator ? (navigator as any).connection.effectiveType : null
5171
+ 'connection' in navigator ? ((navigator as any).connection?.effectiveType ?? null) : null
5116
5172
 
5117
5173
  return await this.store.props.assets.resolve(asset, {
5118
5174
  screenScale: screenScale || 1,
@@ -0,0 +1,98 @@
1
+ import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
2
+ import { TLInstancePresence } from '@tldraw/tlschema'
3
+ import { maxBy } from '@tldraw/utils'
4
+ import {
5
+ getCollaboratorStateFromElapsedTime,
6
+ shouldShowCollaborator,
7
+ } from '../../../utils/collaboratorState'
8
+ import type { Editor } from '../../Editor'
9
+
10
+ /**
11
+ * Tracks remote peers and exposes the collaborator-related queries used by the
12
+ * editor and its overlays. Encapsulates the visibility clock that periodically
13
+ * re-evaluates which collaborators should be visible based on activity.
14
+ *
15
+ * Accessed via {@link Editor.collaborators}.
16
+ *
17
+ * @public
18
+ */
19
+ export class CollaboratorsManager {
20
+ constructor(private readonly editor: Editor) {
21
+ // Editor disposes `editor.timers` on its own teardown, so the interval is
22
+ // automatically cleared when the editor is disposed.
23
+ editor.timers.setInterval(() => {
24
+ this._visibilityClock.set(Date.now())
25
+ }, editor.options.collaboratorCheckIntervalMs)
26
+ }
27
+
28
+ /**
29
+ * Drives reactive re-evaluation of {@link CollaboratorsManager.getVisibleCollaborators}.
30
+ * Ticked on a fixed interval so callers don't need to manage their own activity timers.
31
+ */
32
+ private readonly _visibilityClock = atom('collaboratorVisibilityClock', Date.now())
33
+
34
+ @computed
35
+ private _getCollaboratorsQuery() {
36
+ return this.editor.store.query.records('instance_presence', () => ({
37
+ userId: { neq: this.editor.user.getId() },
38
+ }))
39
+ }
40
+
41
+ /**
42
+ * Returns a list of presence records for all peer collaborators.
43
+ * This will return the latest presence record for each connected user.
44
+ */
45
+ @computed
46
+ getCollaborators(): TLInstancePresence[] {
47
+ const allPresenceRecords = this._getCollaboratorsQuery().get()
48
+ if (!allPresenceRecords.length) return EMPTY_ARRAY
49
+ const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort()
50
+ return userIds.map((id) => {
51
+ const latestPresence = maxBy(
52
+ allPresenceRecords.filter((c) => c.userId === id),
53
+ (p) => p.lastActivityTimestamp ?? 0
54
+ )
55
+ return latestPresence!
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Returns a list of presence records for all peer collaborators on the current page.
61
+ * This will return the latest presence record for each connected user.
62
+ */
63
+ @computed
64
+ getCollaboratorsOnCurrentPage(): TLInstancePresence[] {
65
+ const currentPageId = this.editor.getCurrentPageId()
66
+ return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
67
+ }
68
+
69
+ /**
70
+ * Returns a list of presence records for peer collaborators who should currently be
71
+ * shown in the UI. Filters {@link CollaboratorsManager.getCollaborators} by activity
72
+ * state (active / idle / inactive) and visibility rules such as following and
73
+ * highlighted users. Re-evaluates on the visibility clock, so callers don't need to
74
+ * drive their own activity timer.
75
+ */
76
+ @computed
77
+ getVisibleCollaborators(): TLInstancePresence[] {
78
+ this._visibilityClock.get()
79
+ const now = Date.now()
80
+ return this.getCollaborators().filter((presence) => {
81
+ // Treat a missing `lastActivityTimestamp` as "active right now" (elapsed = 0)
82
+ // so newly-joined peers aren't immediately classified as idle/inactive.
83
+ const elapsed = Math.max(0, now - (presence.lastActivityTimestamp ?? now))
84
+ const state = getCollaboratorStateFromElapsedTime(this.editor, elapsed)
85
+ return shouldShowCollaborator(this.editor, presence, state)
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Returns a list of presence records for peer collaborators who should currently be
91
+ * shown in the UI, filtered to those on the current page.
92
+ */
93
+ @computed
94
+ getVisibleCollaboratorsOnCurrentPage(): TLInstancePresence[] {
95
+ const currentPageId = this.editor.getCurrentPageId()
96
+ return this.getVisibleCollaborators().filter((c) => c.currentPageId === currentPageId)
97
+ }
98
+ }
@@ -136,6 +136,13 @@ export const DEFAULT_THEME: TLTheme = {
136
136
  solid: '#fcfffe',
137
137
  cursor: 'black',
138
138
  noteBorder: 'rgb(144, 144, 144)',
139
+ snap: 'hsl(0, 76%, 60%)',
140
+ selectionStroke: 'hsl(214, 84%, 56%)',
141
+ selectionFill: 'hsl(210, 100%, 56%, 24%)',
142
+ brushFill: 'hsl(0, 0%, 56%, 10.2%)',
143
+ brushStroke: 'hsl(0, 0%, 56%, 25.1%)',
144
+ selectedContrast: '#ffffff',
145
+ laser: 'hsl(0, 100%, 50%)',
139
146
  black: {
140
147
  solid: '#1d1d1d',
141
148
  fill: '#1d1d1d',
@@ -351,6 +358,13 @@ export const DEFAULT_THEME: TLTheme = {
351
358
  negativeSpace: 'hsl(240, 5%, 6.5%)',
352
359
  solid: '#010403',
353
360
  cursor: 'white',
361
+ snap: 'hsl(0, 76%, 60%)',
362
+ selectionStroke: 'hsl(214, 84%, 56%)',
363
+ selectionFill: 'hsl(209, 100%, 57%, 20%)',
364
+ brushFill: 'hsl(0, 0%, 56%, 10.2%)',
365
+ brushStroke: 'hsl(0, 0%, 56%, 25.1%)',
366
+ selectedContrast: '#ffffff',
367
+ laser: 'hsl(0, 100%, 50%)',
354
368
  noteBorder: 'rgb(20, 20, 20)',
355
369
 
356
370
  black: {
@@ -0,0 +1,183 @@
1
+ import { atom, computed } from '@tldraw/state'
2
+ import { Geometry2d } from '../../primitives/geometry/Geometry2d'
3
+ import { VecLike } from '../../primitives/Vec'
4
+ import type { Editor } from '../Editor'
5
+ import { OverlayUtil, TLOverlay } from './OverlayUtil'
6
+
7
+ /**
8
+ * An active overlay util paired with the overlays it produced for the current
9
+ * editor state. Returned by {@link OverlayManager.getActiveOverlayEntries} so
10
+ * hit-test, render, and debug paths share a single scan per reactive tick.
11
+ *
12
+ * @public
13
+ */
14
+ export interface TLOverlayEntry {
15
+ util: OverlayUtil
16
+ overlays: TLOverlay[]
17
+ }
18
+
19
+ /** @public */
20
+ export class OverlayManager {
21
+ constructor(public readonly editor: Editor) {}
22
+
23
+ /** @internal */
24
+ readonly _overlayUtils = new Map<string, OverlayUtil>()
25
+
26
+ /**
27
+ * Register an overlay util instance. Called during editor construction.
28
+ * @internal
29
+ */
30
+ registerUtil(util: OverlayUtil) {
31
+ const type = (util.constructor as typeof OverlayUtil).type
32
+ if (!type) {
33
+ throw new Error(`Overlay util ${util.constructor.name} is missing a static 'type' property.`)
34
+ }
35
+ if (this._overlayUtils.has(type)) {
36
+ throw new Error(`Duplicate overlay util type: "${type}"`)
37
+ }
38
+ this._overlayUtils.set(type, util)
39
+ }
40
+
41
+ /**
42
+ * Get an overlay util by type string, overlay instance, or by passing
43
+ * a util class as a generic parameter for type-safe lookup.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const util = editor.overlays.getOverlayUtil('brush')
48
+ * const util = editor.overlays.getOverlayUtil<BrushOverlayUtil>('brush')
49
+ * const util = editor.overlays.getOverlayUtil(myOverlay)
50
+ * ```
51
+ *
52
+ * @public
53
+ */
54
+ getOverlayUtil<T extends OverlayUtil>(
55
+ type: T extends OverlayUtil<infer O> ? O['type'] : string
56
+ ): T
57
+ getOverlayUtil<O extends TLOverlay>(overlay: O): OverlayUtil<O>
58
+ getOverlayUtil(arg: string | TLOverlay): OverlayUtil {
59
+ const type = typeof arg === 'string' ? arg : arg.type
60
+ const util = this._overlayUtils.get(type)
61
+ if (!util) throw new Error(`No overlay util found for type: "${type}"`)
62
+ return util
63
+ }
64
+
65
+ /**
66
+ * Returns all registered overlay utils in paint order (ascending zIndex).
67
+ * Utils with the same zIndex preserve their registration order.
68
+ *
69
+ * @public
70
+ */
71
+ @computed getOverlayUtilsInZOrder(): OverlayUtil[] {
72
+ const utils = Array.from(this._overlayUtils.values())
73
+ // Stable sort by zIndex (registration order breaks ties).
74
+ return utils
75
+ .map((util, i) => ({ util, i, z: util.options.zIndex ?? 0 }))
76
+ .sort((a, b) => a.z - b.z || a.i - b.i)
77
+ .map((entry) => entry.util)
78
+ }
79
+
80
+ /**
81
+ * Reactive list of active overlay utils paired with the overlays they
82
+ * produced for the current editor state, in paint order (ascending
83
+ * zIndex). Both the hit-test and render paths read from this single
84
+ * cached scan instead of each re-deriving the active set. Active utils
85
+ * are included even when their `getOverlays()` returns an empty array,
86
+ * since `render()` may still draw non-interactive UI (e.g. the selection
87
+ * bounding box during brushing).
88
+ *
89
+ * @public
90
+ */
91
+ @computed getActiveOverlayEntries(): TLOverlayEntry[] {
92
+ const entries: TLOverlayEntry[] = []
93
+ for (const util of this.getOverlayUtilsInZOrder()) {
94
+ if (!util.isActive()) continue
95
+ entries.push({ util, overlays: util.getOverlays() })
96
+ }
97
+ return entries
98
+ }
99
+
100
+ /**
101
+ * Reactively computed list of all currently active overlays, in paint order.
102
+ * @public
103
+ */
104
+ @computed getCurrentOverlays(): TLOverlay[] {
105
+ const all: TLOverlay[] = []
106
+ for (const { overlays } of this.getActiveOverlayEntries()) {
107
+ all.push(...overlays)
108
+ }
109
+ return all
110
+ }
111
+
112
+ // Hit-test geometry cache keyed by overlay identity. Entries remain valid
113
+ // while getActiveOverlayEntries() keeps returning the same overlay
114
+ // instances; when its reactive deps change, getOverlays() emits fresh
115
+ // objects and stale entries fall out by GC.
116
+ private _geometryCache = new WeakMap<TLOverlay, Geometry2d | null>()
117
+
118
+ /**
119
+ * Get hit-test geometry for an overlay, cached by overlay identity. Lets
120
+ * hit-testing on a pointermove storm skip the per-overlay geometry
121
+ * allocation that {@link OverlayUtil.getGeometry} would otherwise do on
122
+ * every call.
123
+ *
124
+ * @public
125
+ */
126
+ getOverlayGeometry(overlay: TLOverlay): Geometry2d | null {
127
+ const cached = this._geometryCache.get(overlay)
128
+ if (cached !== undefined) return cached
129
+ const util = this.getOverlayUtil(overlay)
130
+ const geometry = util.getGeometry(overlay)
131
+ this._geometryCache.set(overlay, geometry)
132
+ return geometry
133
+ }
134
+
135
+ /**
136
+ * The currently hovered overlay id.
137
+ * @public
138
+ */
139
+ private _hoveredOverlayId = atom<string | null>('hoveredOverlayId', null)
140
+
141
+ getHoveredOverlayId(): string | null {
142
+ return this._hoveredOverlayId.get()
143
+ }
144
+
145
+ getHoveredOverlay(): TLOverlay | null {
146
+ const id = this._hoveredOverlayId.get()
147
+ if (!id) return null
148
+ return this.getCurrentOverlays().find((o) => o.id === id) ?? null
149
+ }
150
+
151
+ setHoveredOverlay(id: string | null) {
152
+ if (id === this._hoveredOverlayId.get()) return
153
+ this._hoveredOverlayId.set(id)
154
+ }
155
+
156
+ /**
157
+ * Hit test all active overlays at a given page point.
158
+ * Returns the topmost overlay whose geometry contains the point, or null.
159
+ * Utils are walked from highest zIndex to lowest so the overlay painted on
160
+ * top also wins the hit test. Within a util, overlays are walked in
161
+ * array order: the first overlay whose geometry contains the point wins,
162
+ * so utils should place highest-priority overlays first in `getOverlays`.
163
+ * Interactive overlays (those with geometry) are checked; non-interactive are skipped.
164
+ *
165
+ * @param point - Point in page coordinates
166
+ * @param margin - Hit test margin
167
+ * @public
168
+ */
169
+ getOverlayAtPoint(point: VecLike, margin = 0): TLOverlay | null {
170
+ const entries = this.getActiveOverlayEntries()
171
+ for (let i = entries.length - 1; i >= 0; i--) {
172
+ const { overlays } = entries[i]
173
+ for (const overlay of overlays) {
174
+ const geometry = this.getOverlayGeometry(overlay)
175
+ if (!geometry) continue
176
+ if (geometry.hitTestPoint(point, geometry.isFilled ? 0 : margin, true)) {
177
+ return overlay
178
+ }
179
+ }
180
+ }
181
+ return null
182
+ }
183
+ }
@@ -0,0 +1,143 @@
1
+ import { TLCursorType } from '@tldraw/tlschema'
2
+ import { Geometry2d } from '../../primitives/geometry/Geometry2d'
3
+ import type { Editor } from '../Editor'
4
+ import { TLPointerEventInfo } from '../types/event-types'
5
+
6
+ /** @public */
7
+ export interface TLOverlay<Props = Record<string, unknown>> {
8
+ /**
9
+ * Globally unique id for this overlay instance across all overlay utils.
10
+ * Hit-test and hover lookup key on `id` alone, so utils must namespace their
11
+ * ids (e.g. `'selection_fg:top_left'`, `'handle:<shapeId>:<handleId>'`) to
12
+ * avoid colliding with overlays from other utils.
13
+ */
14
+ id: string
15
+ /** The overlay util type that owns this instance */
16
+ type: string
17
+ /** Arbitrary props for the overlay (handle id, corner name, etc.) */
18
+ props: Props
19
+ }
20
+
21
+ /** @public */
22
+ export interface TLOverlayUtilConstructor<U extends OverlayUtil = OverlayUtil> {
23
+ new (editor: Editor): U
24
+ type: string
25
+ configure<T extends TLOverlayUtilConstructor<any>>(
26
+ this: T,
27
+ options: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never
28
+ ): T
29
+ }
30
+
31
+ /** @public */
32
+ export type TLAnyOverlayUtilConstructor = TLOverlayUtilConstructor<any>
33
+
34
+ /**
35
+ * Base class for overlay utilities. Overlays are ephemeral UI elements rendered
36
+ * on top of the canvas (selection handles, rotation corners, shape handles, etc.).
37
+ *
38
+ * Each OverlayUtil defines a type of overlay and knows how to:
39
+ * - Determine when its overlays should be active (predicate)
40
+ * - Produce overlay instances from current editor state
41
+ * - Provide hit-test geometry for interactive overlays
42
+ * - Provide cursor style on hover
43
+ * - Render into a canvas 2D context
44
+ *
45
+ * @public
46
+ */
47
+ export abstract class OverlayUtil<T extends TLOverlay = TLOverlay> {
48
+ constructor(public editor: Editor) {}
49
+ static type: string
50
+
51
+ /**
52
+ * Options for this overlay util. Override this to provide customization options.
53
+ * Use {@link OverlayUtil.configure} to customize existing overlay utils.
54
+ *
55
+ * `zIndex` controls paint and hit-test order across utils — higher numbers
56
+ * paint on top and are hit-tested first. Ties resolve by registration order.
57
+ * Defaults to `0`; built-in utils use larger integers (100, 200, …) with
58
+ * gaps so custom utils can slot between.
59
+ *
60
+ * @public
61
+ */
62
+ options: { zIndex?: number } = {}
63
+
64
+ /**
65
+ * Create a new overlay util class with the given options merged in.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const MyBrush = BrushOverlayUtil.configure({ fill: 'rgba(0,0,255,0.1)' })
70
+ * ```
71
+ *
72
+ * @public
73
+ */
74
+ static configure<T extends TLOverlayUtilConstructor<any>>(
75
+ this: T,
76
+ options: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never
77
+ ): T {
78
+ // @ts-expect-error -- typescript has no idea what's going on here but it's fine
79
+ return class extends this {
80
+ // @ts-expect-error
81
+ options = { ...this.options, ...options }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Whether this overlay util's overlays should currently be active.
87
+ * Checked reactively to determine which overlays exist at any given time.
88
+ */
89
+ abstract isActive(): boolean
90
+
91
+ /**
92
+ * Returns the overlay instances that currently exist.
93
+ * Called only when `isActive()` returns true.
94
+ */
95
+ abstract getOverlays(): T[]
96
+
97
+ /**
98
+ * Returns hit-test geometry for an overlay instance, in page coordinates.
99
+ * Return null for non-interactive overlays (e.g. snap indicators, scribbles).
100
+ */
101
+ getGeometry(_overlay: T): Geometry2d | null {
102
+ return null
103
+ }
104
+
105
+ /**
106
+ * Returns the cursor type to show when hovering this overlay.
107
+ */
108
+ getCursor(_overlay: T): TLCursorType | undefined {
109
+ return undefined
110
+ }
111
+
112
+ /**
113
+ * Called when the user points down on this overlay, before the default
114
+ * routing runs. Acts as an interrupt: define it to take over the event.
115
+ *
116
+ * Return `false` to continue with the default behavior (e.g. the
117
+ * built-in rotate/resize handle transitions or shape-handle dispatch).
118
+ * Return `true` — or nothing at all — to skip the default. In other
119
+ * words, once you override this method you own the event unless you
120
+ * explicitly opt back in by returning `false`.
121
+ */
122
+ onPointerDown?(overlay: T, info: TLPointerEventInfo): boolean | void
123
+
124
+ /**
125
+ * Render all active overlays into the canvas context.
126
+ * The context is already transformed to page space (camera transform applied).
127
+ * Called reactively when overlays or editor state changes.
128
+ */
129
+ render(_ctx: CanvasRenderingContext2D, _overlays: T[]): void {}
130
+
131
+ /**
132
+ * Optional: render all active overlays into the minimap canvas.
133
+ * The context is already transformed to page space (minimap camera applied),
134
+ * so overlays can use the same page-space coordinates as in {@link OverlayUtil.render}.
135
+ *
136
+ * `zoom` is the minimap's screen-pixels-per-page-unit, analogous to
137
+ * `editor.getCamera().z`; use `1 / zoom` for one-minimap-pixel line widths.
138
+ *
139
+ * Most overlays should leave this blank — only overlays that are meaningful
140
+ * at minimap scale (e.g. brushes, collaborator cursors) should opt in.
141
+ */
142
+ renderMinimap(_ctx: CanvasRenderingContext2D, _overlays: T[], _zoom: number): void {}
143
+ }