@tldraw/editor 5.1.1 → 5.2.0-canary.019da1aa690a

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 (171) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +52 -50
  3. package/dist-cjs/index.js +4 -4
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/MenuClickCapture.js +8 -5
  6. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
  8. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  9. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
  12. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
  13. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +121 -55
  16. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  17. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
  18. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  19. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
  20. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  21. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
  22. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  24. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
  26. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
  28. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
  30. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
  32. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
  34. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  35. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
  36. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  37. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
  38. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  39. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
  40. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  41. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  42. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  43. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -0
  44. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  45. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  46. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  47. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  48. package/dist-cjs/lib/hooks/useCanvasEvents.js +14 -7
  49. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  51. package/dist-cjs/lib/license/LicenseProvider.js +3 -1
  52. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  53. package/dist-cjs/lib/primitives/utils.js +2 -2
  54. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  55. package/dist-cjs/lib/utils/dom.js +5 -3
  56. package/dist-cjs/lib/utils/dom.js.map +2 -2
  57. package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
  58. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  59. package/dist-cjs/lib/utils/pointer.js +32 -0
  60. package/dist-cjs/lib/utils/pointer.js.map +7 -0
  61. package/dist-cjs/version.js +3 -3
  62. package/dist-cjs/version.js.map +1 -1
  63. package/dist-esm/index.d.mts +52 -50
  64. package/dist-esm/index.mjs +5 -7
  65. package/dist-esm/index.mjs.map +2 -2
  66. package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
  67. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
  69. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  70. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
  71. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
  72. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
  73. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
  74. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
  75. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
  76. package/dist-esm/lib/editor/Editor.mjs +121 -55
  77. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  78. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
  79. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  80. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
  81. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  82. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
  83. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  84. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  85. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  87. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  88. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  89. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  90. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
  91. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  92. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
  93. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  94. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
  95. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  96. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
  97. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  98. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
  99. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  100. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  101. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  102. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  103. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  104. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -0
  105. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  106. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  107. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  108. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  109. package/dist-esm/lib/hooks/useCanvasEvents.mjs +14 -7
  110. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  111. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  112. package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
  113. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/utils.mjs +2 -2
  115. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  116. package/dist-esm/lib/utils/dom.mjs +5 -3
  117. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  118. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
  119. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  120. package/dist-esm/lib/utils/pointer.mjs +12 -0
  121. package/dist-esm/lib/utils/pointer.mjs.map +7 -0
  122. package/dist-esm/version.mjs +3 -3
  123. package/dist-esm/version.mjs.map +1 -1
  124. package/editor.css +5 -3
  125. package/package.json +11 -8
  126. package/src/index.ts +2 -5
  127. package/src/lib/components/MenuClickCapture.tsx +8 -4
  128. package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
  129. package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
  130. package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
  131. package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
  132. package/src/lib/editor/Editor.ts +168 -72
  133. package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
  134. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  135. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
  136. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  137. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  138. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
  139. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
  140. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
  141. package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
  142. package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
  143. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  144. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
  145. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
  146. package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
  147. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
  148. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  149. package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
  150. package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
  151. package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
  152. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  153. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  154. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  155. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -0
  156. package/src/lib/editor/tools/StateNode.ts +0 -2
  157. package/src/lib/editor/types/event-types.ts +2 -6
  158. package/src/lib/hooks/useCanvasEvents.ts +19 -12
  159. package/src/lib/hooks/usePresence.ts +2 -2
  160. package/src/lib/license/LicenseProvider.tsx +3 -1
  161. package/src/lib/primitives/utils.ts +1 -1
  162. package/src/lib/utils/dom.ts +5 -3
  163. package/src/lib/utils/getPointerInfo.ts +2 -1
  164. package/src/lib/utils/pointer.test.ts +48 -0
  165. package/src/lib/utils/pointer.ts +18 -0
  166. package/src/version.ts +3 -3
  167. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  168. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  169. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  170. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  171. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
@@ -48,7 +48,11 @@ export class HistoryManager<R extends UnknownRecord> {
48
48
  switch (this.state) {
49
49
  case HistoryRecorderState.Recording:
50
50
  this.pendingDiff.apply(entry.changes)
51
- this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
51
+ // this interceptor runs for every store change, so skip the update when the
52
+ // redo stack is already empty
53
+ if (this.stacks.get().redos.length > 0) {
54
+ this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
55
+ }
52
56
  break
53
57
  case HistoryRecorderState.RecordingPreserveRedoStack:
54
58
  this.pendingDiff.apply(entry.changes)
@@ -79,6 +83,14 @@ export class HistoryManager<R extends UnknownRecord> {
79
83
  return this.stacks.get().redos.length
80
84
  }
81
85
 
86
+ /** @internal */
87
+ private _isReplaying = false
88
+
89
+ /** @internal */
90
+ isReplaying() {
91
+ return this._isReplaying
92
+ }
93
+
82
94
  /** @internal */
83
95
  _isInBatch = false
84
96
 
@@ -115,7 +127,9 @@ export class HistoryManager<R extends UnknownRecord> {
115
127
  // History
116
128
  _undo({ pushToRedoStack, toMark = undefined }: { pushToRedoStack: boolean; toMark?: string }) {
117
129
  const previousState = this.state
130
+ const previousIsReplaying = this._isReplaying
118
131
  this.state = HistoryRecorderState.Paused
132
+ this._isReplaying = true
119
133
  try {
120
134
  let { undos, redos } = this.stacks.get()
121
135
 
@@ -176,11 +190,11 @@ export class HistoryManager<R extends UnknownRecord> {
176
190
  this.pendingDiff.restore(pendingDiff)
177
191
  return this
178
192
  }
179
-
180
193
  this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true })
181
194
  this.store.ensureStoreIsUsable()
182
195
  this.stacks.set({ undos, redos })
183
196
  } finally {
197
+ this._isReplaying = previousIsReplaying
184
198
  this.state = previousState
185
199
  }
186
200
 
@@ -195,7 +209,9 @@ export class HistoryManager<R extends UnknownRecord> {
195
209
 
196
210
  redo() {
197
211
  const previousState = this.state
212
+ const previousIsReplaying = this._isReplaying
198
213
  this.state = HistoryRecorderState.Paused
214
+ this._isReplaying = true
199
215
  try {
200
216
  this.flushPendingDiff()
201
217
 
@@ -224,11 +240,11 @@ export class HistoryManager<R extends UnknownRecord> {
224
240
  break
225
241
  }
226
242
  }
227
-
228
243
  this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true })
229
244
  this.store.ensureStoreIsUsable()
230
245
  this.stacks.set({ undos, redos })
231
246
  } finally {
247
+ this._isReplaying = previousIsReplaying
232
248
  this.state = previousState
233
249
  }
234
250
 
@@ -349,7 +365,16 @@ class PendingDiff<R extends UnknownRecord> {
349
365
 
350
366
  apply(diff: RecordsDiff<R>) {
351
367
  squashRecordDiffsMutable(this.diff, [diff])
352
- this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
368
+ // Recomputing emptiness from the accumulated diff is O(N) (for-in over a
369
+ // dictionary-mode object pays an O(N) key-collection prologue), and this runs on
370
+ // every history interceptor call — e.g. every input tick while resizing many
371
+ // shapes. Updates can never cancel out existing entries during a squash, so the
372
+ // full recompute is only needed when the incoming diff adds or removes records.
373
+ if (hasAnyKey(diff.added) || hasAnyKey(diff.removed)) {
374
+ this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
375
+ } else if (this.isEmptyAtom.__unsafe__getWithoutCapture()) {
376
+ this.isEmptyAtom.set(!hasAnyKey(diff.updated))
377
+ }
353
378
  }
354
379
 
355
380
  debug() {
@@ -357,6 +382,11 @@ class PendingDiff<R extends UnknownRecord> {
357
382
  }
358
383
  }
359
384
 
385
+ function hasAnyKey(obj: object) {
386
+ for (const _ in obj) return true
387
+ return false
388
+ }
389
+
360
390
  type Stack<T> = StackItem<T> | EmptyStackItem<T>
361
391
 
362
392
  function stack<T>(): Stack<T> {
@@ -0,0 +1,61 @@
1
+ import { createTLStore } from '../../../config/createTLStore'
2
+ import { Editor } from '../../Editor'
3
+
4
+ function createTestEditor() {
5
+ const store = createTLStore({})
6
+ store.ensureStoreIsUsable()
7
+ return new Editor({
8
+ store,
9
+ bindingUtils: [],
10
+ shapeUtils: [],
11
+ getContainer: () => document.createElement('div'),
12
+ tools: [],
13
+ })
14
+ }
15
+
16
+ describe('InputsManager', () => {
17
+ let editor: Editor
18
+
19
+ beforeEach(() => {
20
+ editor = createTestEditor()
21
+ })
22
+
23
+ afterEach(() => {
24
+ editor.dispose()
25
+ })
26
+
27
+ it('updates pointer velocity on frame events', () => {
28
+ const point = editor.inputs.getCurrentScreenPoint()
29
+ point.x = 0
30
+ point.y = 0
31
+ editor.emit('frame', 16)
32
+
33
+ point.x = 100
34
+ point.y = 0
35
+ editor.emit('frame', 16)
36
+
37
+ expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
38
+ })
39
+
40
+ it('stops updating pointer velocity after dispose', () => {
41
+ const point = editor.inputs.getCurrentScreenPoint()
42
+ point.x = 0
43
+ point.y = 0
44
+ editor.emit('frame', 16)
45
+
46
+ point.x = 100
47
+ point.y = 0
48
+ editor.emit('frame', 16)
49
+
50
+ const velocityBeforeDispose = editor.inputs.getPointerVelocity().clone()
51
+ expect(velocityBeforeDispose.len()).toBeGreaterThan(0)
52
+
53
+ editor.inputs.dispose()
54
+
55
+ point.x = 200
56
+ point.y = 0
57
+ editor.emit('frame', 16)
58
+
59
+ expect(editor.inputs.getPointerVelocity()).toEqual(velocityBeforeDispose)
60
+ })
61
+ })
@@ -1,6 +1,7 @@
1
1
  import { atom, computed, 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'
4
5
  import { INTERNAL_POINTER_IDS } from '../../../constants'
5
6
  import { Vec } from '../../../primitives/Vec'
6
7
  import { isAccelKey } from '../../../utils/keyboard'
@@ -12,7 +13,19 @@ const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
12
13
 
13
14
  /** @public */
14
15
  export class InputsManager {
15
- constructor(private readonly editor: Editor) {}
16
+ constructor(private readonly editor: Editor) {
17
+ this.editor.on('frame', this._onFrame)
18
+ }
19
+
20
+ /** @internal */
21
+ dispose() {
22
+ this.editor.off('frame', this._onFrame)
23
+ }
24
+
25
+ @bind
26
+ private _onFrame(elapsed: number) {
27
+ this.updatePointerVelocity(elapsed)
28
+ }
16
29
 
17
30
  private _originPagePoint = atom<Vec>('originPagePoint', new Vec())
18
31
  /**
@@ -120,8 +133,7 @@ export class InputsManager {
120
133
  }
121
134
 
122
135
  /**
123
- * Normally you shouldn't need to set the pointer velocity directly, this is set by the tick manager.
124
- * However, this is currently used in tests to fake pointer velocity.
136
+ * Normally you shouldn't need to set the pointer velocity directly. Used in tests to fake pointer velocity.
125
137
  * @param pointerVelocity - The pointer velocity.
126
138
  * @internal
127
139
  */
@@ -460,7 +472,7 @@ export class InputsManager {
460
472
  private _velocityPrevPoint = new Vec()
461
473
 
462
474
  /**
463
- * Update the pointer velocity based on elapsed time. Called by the tick manager.
475
+ * Update the pointer velocity based on elapsed time. Called each frame.
464
476
  * @param elapsed - The time elapsed since the last tick in milliseconds.
465
477
  * @internal
466
478
  */
@@ -46,6 +46,10 @@ export class SpatialIndexManager {
46
46
 
47
47
  private createSpatialIndexComputed() {
48
48
  const shapeHistory = this.editor.store.query.filterHistory('shape')
49
+ // Binding changes can move a shape's derived bounds (e.g. creating or
50
+ // deleting an arrow binding relocates the arrow's body) without
51
+ // touching any shape record, so they must also invalidate the index.
52
+ const bindingHistory = this.editor.store.query.filterHistory('binding')
49
53
 
50
54
  return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
51
55
  if (isUninitialized(_prevValue)) {
@@ -53,8 +57,9 @@ export class SpatialIndexManager {
53
57
  }
54
58
 
55
59
  const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
60
+ const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)
56
61
 
57
- if (shapeDiff === RESET_VALUE) {
62
+ if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
58
63
  return this.rebuildAndBumpEpoch()
59
64
  }
60
65
 
@@ -63,8 +68,10 @@ export class SpatialIndexManager {
63
68
  return this.rebuildAndBumpEpoch()
64
69
  }
65
70
 
66
- if (shapeDiff.length === 0) return this._boundsEpoch
71
+ if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch
67
72
 
73
+ // A binding-only diff passes an empty shape diff: step 1 is a no-op
74
+ // and the step-2 sweep re-checks the indexed bounds of every shape.
68
75
  if (this.processIncrementalUpdate(shapeDiff)) {
69
76
  this._boundsEpoch++
70
77
  }
@@ -82,20 +82,22 @@ const mockEditor = {
82
82
  getContainerDocument: vi.fn(() => mockDocument),
83
83
  } as unknown as Editor
84
84
 
85
- global.Range = vi.fn(() => ({
86
- setStart: vi.fn(),
87
- setEnd: vi.fn(),
88
- getClientRects: vi.fn(() => [
89
- {
90
- width: 10,
91
- height: 16,
92
- left: 0,
93
- top: 0,
94
- right: 10,
95
- bottom: 16,
96
- },
97
- ]),
98
- })) as any
85
+ global.Range = vi.fn(function () {
86
+ return {
87
+ setStart: vi.fn(),
88
+ setEnd: vi.fn(),
89
+ getClientRects: vi.fn(() => [
90
+ {
91
+ width: 10,
92
+ height: 16,
93
+ left: 0,
94
+ top: 0,
95
+ right: 10,
96
+ bottom: 16,
97
+ },
98
+ ]),
99
+ }
100
+ }) as any
99
101
 
100
102
  describe('TextManager', () => {
101
103
  let textManager: TextManager
@@ -2,6 +2,21 @@ import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
2
  import { objectMapKeys } from '@tldraw/utils'
3
3
  import type { Editor } from '../../Editor'
4
4
 
5
+ /**
6
+ * The whole-pixel line-height for a given font size and tldraw's unitless line-height
7
+ * multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it
8
+ * to a whole pixel keeps line spacing identical across rendering engines, which otherwise
9
+ * disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the
10
+ * fraction) and let multi-line text drift apart. Apply it everywhere line-height is used —
11
+ * measurement, on-canvas render, and export — so geometry and rendering agree.
12
+ * See https://github.com/tldraw/tldraw/issues/8970.
13
+ *
14
+ * @public
15
+ */
16
+ export function resolveLineHeightPx(fontSize: number, lineHeight: number): number {
17
+ return Math.round(fontSize * lineHeight)
18
+ }
19
+
5
20
  const fixNewLines = /\r?\n|\r/g
6
21
 
7
22
  function normalizeTextForDom(text: string) {
@@ -154,7 +169,7 @@ export class TextManager {
154
169
  'font-style': opts.fontStyle,
155
170
  'font-weight': opts.fontWeight,
156
171
  'font-size': opts.fontSize + 'px',
157
- 'line-height': opts.lineHeight.toString(),
172
+ 'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
158
173
  padding: opts.padding,
159
174
  'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
160
175
  'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
@@ -382,7 +397,7 @@ export class TextManager {
382
397
  'font-style': opts.fontStyle,
383
398
  'font-weight': opts.fontWeight,
384
399
  'font-size': opts.fontSize + 'px',
385
- 'line-height': opts.lineHeight.toString(),
400
+ 'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
386
401
  width: `${elementWidth}px`,
387
402
  height: 'min-content',
388
403
  'text-align': textAlignmentsForLtr[opts.textAlign],
@@ -1,5 +1,4 @@
1
1
  import { Mock, Mocked, vi } from 'vitest'
2
- import { Vec } from '../../../primitives/Vec'
3
2
  import { Editor } from '../../Editor'
4
3
  import { TickManager } from './TickManager'
5
4
 
@@ -21,17 +20,6 @@ describe('TickManager', () => {
21
20
  let tickManager: TickManager
22
21
  let mockEmit: Mock
23
22
  let mockDisposablesAdd: Mock
24
- let mockInputs: {
25
- _currentScreenPoint: Vec
26
- getCurrentScreenPoint(): Vec
27
- currentScreenPoint: Vec
28
- setCurrentScreenPoint(value: Vec): void
29
- _pointerVelocity: Vec
30
- getPointerVelocity(): Vec
31
- pointerVelocity: Vec
32
- setPointerVelocity(value: Vec): void
33
- updatePointerVelocity(elapsed: number): void
34
- }
35
23
 
36
24
  beforeEach(() => {
37
25
  vi.clearAllMocks()
@@ -52,39 +40,11 @@ describe('TickManager', () => {
52
40
  mockEmit = vi.fn()
53
41
  mockDisposablesAdd = vi.fn()
54
42
 
55
- // Create a mock inputs object with getters and setters
56
- mockInputs = {
57
- _currentScreenPoint: new Vec(100, 100),
58
- getCurrentScreenPoint() {
59
- return this._currentScreenPoint
60
- },
61
- get currentScreenPoint() {
62
- return this.getCurrentScreenPoint()
63
- },
64
- setCurrentScreenPoint(value: Vec) {
65
- this._currentScreenPoint = value
66
- },
67
- _pointerVelocity: new Vec(0, 0),
68
- getPointerVelocity() {
69
- return this._pointerVelocity
70
- },
71
- get pointerVelocity() {
72
- return this.getPointerVelocity()
73
- },
74
- setPointerVelocity(value: Vec) {
75
- this._pointerVelocity = value
76
- },
77
- updatePointerVelocity(_elapsed: number) {
78
- // Mock implementation - no-op for tests
79
- },
80
- }
81
-
82
43
  editor = {
83
44
  emit: mockEmit,
84
45
  disposables: {
85
46
  add: mockDisposablesAdd,
86
47
  },
87
- inputs: mockInputs as unknown as Editor['inputs'],
88
48
  } as unknown as Mocked<Editor>
89
49
 
90
50
  tickManager = new TickManager(editor)
@@ -40,7 +40,6 @@ export class TickManager {
40
40
  const elapsed = now - this.now
41
41
  this.now = now
42
42
 
43
- this.editor.inputs.updatePointerVelocity(elapsed)
44
43
  this.editor.emit('frame', elapsed)
45
44
  this.editor.emit('tick', elapsed)
46
45
  this.cancelRaf = throttleToNextFrame(this.tick)
@@ -1,4 +1,5 @@
1
1
  import { atom } from '@tldraw/state'
2
+ import { createUserId } from '@tldraw/tlschema'
2
3
  import { Mocked, vi } from 'vitest'
3
4
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
4
5
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
@@ -296,8 +297,17 @@ describe('UserPreferencesManager', () => {
296
297
  userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
297
298
  })
298
299
 
299
- describe('getId', () => {
300
- it('should return user id', () => {
300
+ describe('getExternalId / getRecordId', () => {
301
+ it('should return the raw external user id', () => {
302
+ expect(userPreferencesManager.getExternalId()).toBe(mockUserPreferences.id)
303
+ })
304
+
305
+ it('should return the prefixed record id', () => {
306
+ expect(userPreferencesManager.getRecordId()).toBe(createUserId(mockUserPreferences.id))
307
+ })
308
+
309
+ it('getId() still returns the external id', () => {
310
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
301
311
  expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
302
312
  })
303
313
  })
@@ -1,4 +1,5 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
+ import { createUserId, TLUserId } from '@tldraw/tlschema'
2
3
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
3
4
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
4
5
  import { getGlobalWindow } from '../../../utils/dom'
@@ -39,7 +40,7 @@ export class UserPreferencesManager {
39
40
  }
40
41
  @computed getUserPreferences() {
41
42
  return {
42
- id: this.getId(),
43
+ id: this.getExternalId(),
43
44
  name: this.getName(),
44
45
  locale: this.getLocale(),
45
46
  color: this.getColor(),
@@ -89,10 +90,34 @@ export class UserPreferencesManager {
89
90
  )
90
91
  }
91
92
 
92
- @computed getId() {
93
+ /**
94
+ * The current user's raw, app-provided id — the value set in the user's
95
+ * {@link @tldraw/editor#TLUserPreferences}. Use this when you need the id your application
96
+ * assigned to the user. To compare against or look up store records, use
97
+ * {@link UserPreferencesManager.getRecordId} instead.
98
+ */
99
+ @computed getExternalId(): string {
93
100
  return this.user.userPreferences.get().id
94
101
  }
95
102
 
103
+ /**
104
+ * @deprecated Use {@link UserPreferencesManager.getExternalId} for the raw app-provided id, or
105
+ * {@link UserPreferencesManager.getRecordId} for the prefixed `TLUserId` record id.
106
+ */
107
+ @computed getId() {
108
+ return this.getExternalId()
109
+ }
110
+
111
+ /**
112
+ * The current user's id as a tldraw {@link @tldraw/tlschema#TLUserId} record id (prefixed
113
+ * with `user:`). Use this when comparing against or looking up store records, such as a
114
+ * presence record's `userId` or `followingUserId`. For the raw, app-provided id, use
115
+ * {@link UserPreferencesManager.getExternalId}.
116
+ */
117
+ @computed getRecordId(): TLUserId {
118
+ return createUserId(this.getExternalId())
119
+ }
120
+
96
121
  @computed getName() {
97
122
  return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
98
123
  }
@@ -0,0 +1,86 @@
1
+ import { createComputedCache } from '@tldraw/store'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
+ import type { Editor } from '../Editor'
4
+
5
+ const indicatorPathCache = createComputedCache(
6
+ 'shapeIndicatorPath',
7
+ (editor: Editor, shape: TLShape) => {
8
+ const util = editor.getShapeUtil(shape)
9
+ return util.getIndicatorPath(shape)
10
+ },
11
+ {
12
+ areRecordsEqual(a, b) {
13
+ return a.props === b.props
14
+ },
15
+ }
16
+ )
17
+
18
+ /**
19
+ * Combine every batchable shape indicator into a single page-space `Path2D` and
20
+ * emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
21
+ * arrows with labels or complex arrowheads) can't be batched — they still
22
+ * stroke individually inside a save/restore with `ctx.clip` applied.
23
+ *
24
+ * Shared by any overlay util that paints shape indicators (e.g. collaborator
25
+ * selections).
26
+ *
27
+ * @public
28
+ */
29
+ export function strokeShapeIndicators(
30
+ editor: Editor,
31
+ ctx: CanvasRenderingContext2D,
32
+ shapeIds: TLShapeId[]
33
+ ): void {
34
+ if (shapeIds.length === 0) return
35
+
36
+ const batched = new Path2D()
37
+
38
+ for (const shapeId of shapeIds) {
39
+ const shape = editor.getShape(shapeId)
40
+ if (!shape || shape.isLocked) continue
41
+
42
+ const pageTransform = editor.getShapePageTransform(shape)
43
+ if (!pageTransform) continue
44
+
45
+ const indicatorPath = indicatorPathCache.get(editor, shape.id)
46
+ if (!indicatorPath) continue
47
+
48
+ if (indicatorPath instanceof Path2D) {
49
+ batched.addPath(indicatorPath, pageTransform)
50
+ continue
51
+ }
52
+
53
+ const { path, clipPath, additionalPaths } = indicatorPath
54
+
55
+ if (!clipPath) {
56
+ batched.addPath(path, pageTransform)
57
+ if (additionalPaths) {
58
+ for (const p of additionalPaths) batched.addPath(p, pageTransform)
59
+ }
60
+ continue
61
+ }
62
+
63
+ // Clipped case: fall back to an individual stroke. Rare (arrows with
64
+ // labels / complex arrowheads), so the extra save/restore/stroke
65
+ // pair per such shape isn't worth batching away.
66
+ ctx.save()
67
+ ctx.transform(
68
+ pageTransform.a,
69
+ pageTransform.b,
70
+ pageTransform.c,
71
+ pageTransform.d,
72
+ pageTransform.e,
73
+ pageTransform.f
74
+ )
75
+ ctx.save()
76
+ ctx.clip(clipPath, 'evenodd')
77
+ ctx.stroke(path)
78
+ ctx.restore()
79
+ if (additionalPaths) {
80
+ for (const p of additionalPaths) ctx.stroke(p)
81
+ }
82
+ ctx.restore()
83
+ }
84
+
85
+ ctx.stroke(batched)
86
+ }
@@ -77,6 +77,10 @@ export class Pointing extends StateNode {
77
77
  this.cancel()
78
78
  }
79
79
 
80
+ override onLongPress() {
81
+ if (this.editor.getInstanceState().isCoarsePointer) this.cancel()
82
+ }
83
+
80
84
  complete() {
81
85
  const originPagePoint = this.editor.inputs.getOriginPagePoint()
82
86
 
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
268
268
  onLongPress?(info: TLPointerEventInfo): void
269
269
  onPointerUp?(info: TLPointerEventInfo): void
270
270
  onDoubleClick?(info: TLClickEventInfo): void
271
- onTripleClick?(info: TLClickEventInfo): void
272
- onQuadrupleClick?(info: TLClickEventInfo): void
273
271
  onRightClick?(info: TLPointerEventInfo): void
274
272
  onMiddleClick?(info: TLPointerEventInfo): void
275
273
  onKeyDown?(info: TLKeyboardEventInfo): void
@@ -24,7 +24,7 @@ export type TLPointerEventName =
24
24
  | 'middle_click'
25
25
 
26
26
  /** @public */
27
- export type TLCLickEventName = 'double_click' | 'triple_click' | 'quadruple_click'
27
+ export type TLCLickEventName = 'double_click'
28
28
 
29
29
  /** @public */
30
30
  export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
@@ -72,7 +72,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
72
72
  point: VecLike
73
73
  pointerId: number
74
74
  button: number
75
- phase: 'down' | 'up' | 'settle'
75
+ phase: 'down' | 'up' | 'settle-down' | 'settle-up'
76
76
  } & TLPointerEventTarget
77
77
 
78
78
  /** @public */
@@ -173,8 +173,6 @@ export interface TLEventHandlers {
173
173
  onLongPress: TLPointerEvent
174
174
  onRightClick: TLPointerEvent
175
175
  onDoubleClick: TLClickEvent
176
- onTripleClick: TLClickEvent
177
- onQuadrupleClick: TLClickEvent
178
176
  onMiddleClick: TLPointerEvent
179
177
  onPointerUp: TLPointerEvent
180
178
  onKeyDown: TLKeyboardEvent
@@ -206,7 +204,5 @@ export const EVENT_NAME_MAP: Record<
206
204
  complete: 'onComplete',
207
205
  interrupt: 'onInterrupt',
208
206
  double_click: 'onDoubleClick',
209
- triple_click: 'onTripleClick',
210
- quadruple_click: 'onQuadrupleClick',
211
207
  tick: 'onTick',
212
208
  }