@tldraw/editor 5.2.0-next.ee0fa4d6244f → 5.2.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 (189) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +117 -60
  3. package/dist-cjs/index.js +7 -8
  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/Shape.js +47 -1
  8. package/dist-cjs/lib/components/Shape.js.map +3 -3
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -16
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +131 -57
  12. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  13. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  14. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
  16. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
  18. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +28 -4
  20. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +3 -3
  21. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
  22. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
  24. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
  28. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
  30. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
  32. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  34. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  35. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  36. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +4 -0
  37. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  38. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  39. package/dist-cjs/lib/{hooks/useTransform.js → editor/tools/cancelShapeCreationOnLongPress.js} +8 -23
  40. package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js.map +7 -0
  41. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  42. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  43. package/dist-cjs/lib/globals/environment.js +6 -1
  44. package/dist-cjs/lib/globals/environment.js.map +2 -2
  45. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useCanvasEvents.js +17 -8
  47. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  48. package/dist-cjs/lib/hooks/useEditorComponents.js +0 -1
  49. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useScreenBounds.js +1 -0
  52. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  53. package/dist-cjs/lib/license/LicenseManager.js +5 -1
  54. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  55. package/dist-cjs/lib/primitives/Vec.js +3 -2
  56. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  57. package/dist-cjs/lib/utils/dom.js +12 -0
  58. package/dist-cjs/lib/utils/dom.js.map +2 -2
  59. package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
  60. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  61. package/dist-cjs/lib/utils/pointer.js +37 -0
  62. package/dist-cjs/lib/utils/pointer.js.map +7 -0
  63. package/dist-cjs/version.js +3 -3
  64. package/dist-cjs/version.js.map +1 -1
  65. package/dist-esm/index.d.mts +117 -60
  66. package/dist-esm/index.mjs +9 -13
  67. package/dist-esm/index.mjs.map +2 -2
  68. package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
  69. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  70. package/dist-esm/lib/components/Shape.mjs +48 -2
  71. package/dist-esm/lib/components/Shape.mjs.map +3 -3
  72. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -16
  73. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  74. package/dist-esm/lib/editor/Editor.mjs +131 -57
  75. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  76. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  77. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  79. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  81. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +28 -4
  83. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +3 -3
  84. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
  85. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
  87. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  88. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
  89. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  90. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
  91. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  92. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
  93. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  94. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  95. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  96. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  97. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  98. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  99. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +4 -0
  100. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  101. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  102. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs +8 -0
  103. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs.map +7 -0
  104. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  105. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  106. package/dist-esm/lib/globals/environment.mjs +6 -1
  107. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  108. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  109. package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -8
  110. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  111. package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -1
  112. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  113. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  114. package/dist-esm/lib/hooks/useScreenBounds.mjs +1 -0
  115. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  116. package/dist-esm/lib/license/LicenseManager.mjs +5 -1
  117. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  118. package/dist-esm/lib/primitives/Vec.mjs +3 -2
  119. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  120. package/dist-esm/lib/utils/dom.mjs +12 -0
  121. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  122. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
  123. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  124. package/dist-esm/lib/utils/pointer.mjs +17 -0
  125. package/dist-esm/lib/utils/pointer.mjs.map +7 -0
  126. package/dist-esm/version.mjs +3 -3
  127. package/dist-esm/version.mjs.map +1 -1
  128. package/editor.css +15 -3
  129. package/package.json +11 -8
  130. package/src/index.ts +4 -10
  131. package/src/lib/components/MenuClickCapture.tsx +8 -4
  132. package/src/lib/components/Shape.tsx +88 -1
  133. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -17
  134. package/src/lib/editor/Editor.test.ts +36 -0
  135. package/src/lib/editor/Editor.ts +204 -75
  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 +37 -10
  143. package/src/lib/editor/managers/FontManager/FontManager.ts +46 -1
  144. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  145. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
  146. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
  147. package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
  148. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
  149. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  150. package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
  151. package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
  152. package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
  153. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  154. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  155. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  156. package/src/lib/editor/shapes/ShapeUtil.ts +41 -0
  157. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +5 -0
  158. package/src/lib/editor/tools/StateNode.ts +0 -2
  159. package/src/lib/editor/tools/cancelShapeCreationOnLongPress.ts +31 -0
  160. package/src/lib/editor/types/event-types.ts +9 -6
  161. package/src/lib/globals/environment.ts +7 -0
  162. package/src/lib/hooks/EditorComponentsContext.tsx +0 -2
  163. package/src/lib/hooks/useCanvasEvents.ts +24 -12
  164. package/src/lib/hooks/useEditorComponents.tsx +0 -1
  165. package/src/lib/hooks/usePresence.ts +2 -2
  166. package/src/lib/hooks/useScreenBounds.ts +3 -0
  167. package/src/lib/license/LicenseManager.test.ts +24 -16
  168. package/src/lib/license/LicenseManager.ts +8 -2
  169. package/src/lib/primitives/Vec.ts +5 -2
  170. package/src/lib/test/ContentElementSlot.test.tsx +201 -0
  171. package/src/lib/utils/dom.ts +34 -0
  172. package/src/lib/utils/getPointerInfo.ts +2 -1
  173. package/src/lib/utils/pointer.test.ts +76 -0
  174. package/src/lib/utils/pointer.ts +42 -0
  175. package/src/version.ts +3 -3
  176. package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js +0 -49
  177. package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js.map +0 -7
  178. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  179. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  180. package/dist-cjs/lib/hooks/useTransform.js.map +0 -7
  181. package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs +0 -19
  182. package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs.map +0 -7
  183. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  184. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  185. package/dist-esm/lib/hooks/useTransform.mjs +0 -23
  186. package/dist-esm/lib/hooks/useTransform.mjs.map +0 -7
  187. package/src/lib/components/default-components/DefaultSelectionBackground.tsx +0 -25
  188. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
  189. package/src/lib/hooks/useTransform.ts +0 -30
@@ -409,9 +409,6 @@ export class Editor extends EventEmitter<TLEventMap> {
409
409
  this._tickManager = new TickManager(this)
410
410
  this.disposables.add(() => this._tickManager.dispose())
411
411
  this.disposables.add(() => {
412
- // Reset camera state to 'idle' so the store isn't left stuck at 'moving'
413
- // when tick events stop (e.g. React strict mode disposes while camera is moving)
414
- this.off('tick', this._decayCameraStateTimeout)
415
412
  this._setCameraState('idle')
416
413
  })
417
414
 
@@ -419,6 +416,7 @@ export class Editor extends EventEmitter<TLEventMap> {
419
416
  this.disposables.add(() => this.fonts.dispose())
420
417
 
421
418
  this.inputs = new InputsManager(this)
419
+ this.disposables.add(() => this.inputs.dispose())
422
420
  this.performance = new PerformanceManager(this)
423
421
  this.disposables.add(() => this.performance.dispose())
424
422
  this.collaborators = new CollaboratorsManager(this)
@@ -1179,6 +1177,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1179
1177
  this.store.dispose()
1180
1178
  this.isDisposed = true
1181
1179
  this.emit('dispose')
1180
+ this.removeAllListeners()
1182
1181
  }
1183
1182
 
1184
1183
  /* ------------------ Themes (shadowing the theme manager) ------------------ */
@@ -1551,6 +1550,15 @@ export class Editor extends EventEmitter<TLEventMap> {
1551
1550
  return this.history.getMarkIdMatching(idSubstring)
1552
1551
  }
1553
1552
 
1553
+ /**
1554
+ * Whether the editor is currently replaying history (i.e. an undo or redo is being applied).
1555
+ *
1556
+ * @internal
1557
+ */
1558
+ isReplayingHistory(): boolean {
1559
+ return this.history.isReplaying()
1560
+ }
1561
+
1554
1562
  /**
1555
1563
  * Coalesces all changes since the given mark into a single change, removing any intermediate marks.
1556
1564
  *
@@ -3068,8 +3076,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3068
3076
  return baseCamera
3069
3077
  }
3070
3078
 
3071
- private _getFollowingPresence(targetUserId: string | null) {
3072
- const visited = [this.user.getId()]
3079
+ private _getFollowingPresence(targetUserId: TLUserId | null) {
3080
+ const visited = [this.user.getRecordId()]
3073
3081
  const collaborators = this.getCollaborators()
3074
3082
  let leaderPresence = null as null | TLInstancePresence
3075
3083
  while (targetUserId && !visited.includes(targetUserId)) {
@@ -3336,6 +3344,15 @@ export class Editor extends EventEmitter<TLEventMap> {
3336
3344
 
3337
3345
  let { x, y, z = currentCamera.z } = point
3338
3346
 
3347
+ // `requested` kept the caller's focal point (e.g. the cursor) fixed at
3348
+ // zoom `rz`. When `rz` gets clamped, keep that same focal point fixed at
3349
+ // the clamped zoom `z` rather than snapping to the viewport center.
3350
+ const preserveFocalPoint = (current: number, requested: number, rz: number, z: number) => {
3351
+ const cz = currentCamera.z
3352
+ if (rz === cz) return current
3353
+ return current + ((requested - current) * (1 / z - 1 / cz)) / (1 / rz - 1 / cz)
3354
+ }
3355
+
3339
3356
  // If force is true, then we'll set the camera to the point regardless of
3340
3357
  // the camera options, so that we can handle gestures that permit elasticity
3341
3358
  // or decay, or animations that occur while the camera is locked.
@@ -3378,17 +3395,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3378
3395
  }
3379
3396
 
3380
3397
  if (z < minZ || z > maxZ) {
3381
- // We're trying to zoom out past the minimum zoom level,
3382
- // or in past the maximum zoom level, so stop the camera
3383
- // but keep the current center
3384
- const { x: cx, y: cy, z: cz } = currentCamera
3385
- const cxA = -cx + vsb.w / cz / 2
3386
- const cyA = -cy + vsb.h / cz / 2
3398
+ // We're trying to zoom out past the minimum zoom level, or in
3399
+ // past the maximum zoom level, so clamp the zoom while keeping
3400
+ // the caller's focal point fixed. Axis constraints below still
3401
+ // apply on top of this.
3402
+ const rz = z
3387
3403
  z = clamp(z, minZ, maxZ)
3388
- const cxB = -cx + vsb.w / z / 2
3389
- const cyB = -cy + vsb.h / z / 2
3390
- x = cx + cxB - cxA
3391
- y = cy + cyB - cyA
3404
+ x = preserveFocalPoint(currentCamera.x, x, rz, z)
3405
+ y = preserveFocalPoint(currentCamera.y, y, rz, z)
3392
3406
  }
3393
3407
 
3394
3408
  // Calculate available space
@@ -3477,12 +3491,12 @@ export class Editor extends EventEmitter<TLEventMap> {
3477
3491
  }
3478
3492
  }
3479
3493
  } else {
3480
- // constrain the zoom, preserving the center
3494
+ // constrain the zoom, keeping the caller's focal point fixed
3481
3495
  if (z > zoomMax || z < zoomMin) {
3482
- const { x: cx, y: cy, z: cz } = currentCamera
3496
+ const rz = z
3483
3497
  z = clamp(z, zoomMin, zoomMax)
3484
- x = cx + (-cx + vsb.w / z / 2) - (-cx + vsb.w / cz / 2)
3485
- y = cy + (-cy + vsb.h / z / 2) - (-cy + vsb.h / cz / 2)
3498
+ x = preserveFocalPoint(currentCamera.x, x, rz, z)
3499
+ y = preserveFocalPoint(currentCamera.y, y, rz, z)
3486
3500
  }
3487
3501
  }
3488
3502
  }
@@ -4023,16 +4037,34 @@ export class Editor extends EventEmitter<TLEventMap> {
4023
4037
 
4024
4038
  this.once('stop-camera-animation', cancel)
4025
4039
 
4040
+ const dirZ = direction.z ?? 0
4041
+
4026
4042
  const moveCamera = (elapsed: number) => {
4027
4043
  const { x: cx, y: cy, z: cz } = this.getCamera()
4028
- const movementVec = Vec.Mul(direction, (currentSpeed * elapsed) / cz)
4044
+
4045
+ // Pan movement from x/y direction
4046
+ const dx = (direction.x * (currentSpeed * elapsed)) / cz
4047
+ const dy = (direction.y * (currentSpeed * elapsed)) / cz
4048
+
4049
+ let newCx = cx + dx
4050
+ let newCy = cy + dy
4051
+ let newCz = cz
4052
+
4053
+ // animate zoom if z direction is passed in
4054
+ if (dirZ !== 0) {
4055
+ newCz = cz * (1 + dirZ * currentSpeed * elapsed)
4056
+ // Adjust x/y to keep the viewport center fixed while zooming
4057
+ const center = this.getViewportScreenCenter()
4058
+ newCx += center.x / newCz - center.x / cz
4059
+ newCy += center.y / newCz - center.y / cz
4060
+ }
4029
4061
 
4030
4062
  // Apply friction
4031
4063
  currentSpeed *= 1 - friction
4032
4064
  if (currentSpeed < speedThreshold) {
4033
4065
  cancel()
4034
4066
  } else {
4035
- this._setCamera(new Vec(cx + movementVec.x, cy + movementVec.y, cz))
4067
+ this._setCamera(new Vec(newCx, newCy, newCz))
4036
4068
  }
4037
4069
  }
4038
4070
 
@@ -4054,7 +4086,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4054
4086
  * @param opts - The camera move options.
4055
4087
  * @public
4056
4088
  */
4057
- zoomToUser(userId: string, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this {
4089
+ zoomToUser(userId: TLUserId, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this {
4058
4090
  const presence = this.getCollaborators().find((c) => c.userId === userId)
4059
4091
 
4060
4092
  if (!presence) return this
@@ -4133,6 +4165,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4133
4165
  }
4134
4166
 
4135
4167
  const doc = this.getContainerDocument()
4168
+ // If the container's document has been torn down (e.g. an iframe being
4169
+ // removed), its body is null and there's nothing meaningful to measure.
4170
+ if (!doc.body) return this
4171
+
4136
4172
  const insets = [
4137
4173
  // top
4138
4174
  screenBounds.minY !== 0,
@@ -4394,7 +4430,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4394
4430
 
4395
4431
  /**
4396
4432
  * Collect user IDs referenced by a set of shapes via shape-specific props
4397
- * (e.g. `textFirstEditedBy` on notes).
4433
+ * (e.g. `textLastEditedBy` on notes).
4398
4434
  *
4399
4435
  * @internal
4400
4436
  */
@@ -4426,11 +4462,11 @@ export class Editor extends EventEmitter<TLEventMap> {
4426
4462
  *
4427
4463
  * @public
4428
4464
  */
4429
- startFollowingUser(userId: string): this {
4465
+ startFollowingUser(userId: TLUserId): this {
4430
4466
  // if we were already following someone, stop following them
4431
4467
  this.stopFollowingUser()
4432
4468
 
4433
- const thisUserId = this.user.getId()
4469
+ const thisUserId = this.user.getExternalId()
4434
4470
 
4435
4471
  if (!thisUserId) {
4436
4472
  console.warn('You should set the userId for the current instance before following a user')
@@ -8078,32 +8114,53 @@ export class Editor extends EventEmitter<TLEventMap> {
8078
8114
  * @public
8079
8115
  */
8080
8116
  resizeShape(shape: TLShapeId | TLShape, scale: VecLike, opts: TLResizeShapeOptions = {}): this {
8117
+ const partial = this.getResizeShapePartial(shape, scale, opts)
8118
+ if (partial) this.updateShapes([partial])
8119
+ return this
8120
+ }
8121
+
8122
+ /**
8123
+ * Get the update for a resized shape without committing it to the store. Interactions that
8124
+ * resize many shapes at once use this to collect all of the updates and commit them in a
8125
+ * single batch. Returns null when there is nothing to update.
8126
+ *
8127
+ * Shapes that are rotated out of alignment with the scale axis cannot be resized with a
8128
+ * single update; those shapes are resized immediately (as `resizeShape` would do) and null
8129
+ * is returned.
8130
+ *
8131
+ * @internal
8132
+ */
8133
+ getResizeShapePartial(
8134
+ shape: TLShapeId | TLShape,
8135
+ scale: VecLike,
8136
+ opts: TLResizeShapeOptions = {}
8137
+ ): TLShapePartial | null {
8081
8138
  const id = typeof shape === 'string' ? shape : shape.id
8082
- if (this.getIsReadonly()) return this
8139
+ if (this.getIsReadonly()) return null
8083
8140
 
8084
8141
  if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y)
8085
8142
  if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1)
8086
8143
 
8087
8144
  const initialShape = opts.initialShape ?? this.getShape(id)
8088
- if (!initialShape) return this
8145
+ if (!initialShape) return null
8089
8146
 
8090
8147
  const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center
8091
- if (!scaleOrigin) return this
8148
+ if (!scaleOrigin) return null
8092
8149
 
8093
8150
  const pageTransform = opts.initialPageTransform
8094
8151
  ? Mat.Cast(opts.initialPageTransform)
8095
8152
  : this.getShapePageTransform(id)
8096
- if (!pageTransform) return this
8153
+ if (!pageTransform) return null
8097
8154
 
8098
8155
  const pageRotation = pageTransform.rotation()
8099
8156
 
8100
- if (pageRotation == null) return this
8157
+ if (pageRotation == null) return null
8101
8158
 
8102
8159
  const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
8103
8160
 
8104
8161
  const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
8105
8162
 
8106
- if (!initialBounds) return this
8163
+ if (!initialBounds) return null
8107
8164
 
8108
8165
  const isAspectRatioLocked =
8109
8166
  opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape)
@@ -8113,7 +8170,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8113
8170
  // from whichever axis is being scaled the least, to avoid the shape getting bigger
8114
8171
  // than the bounds of the selection
8115
8172
  // const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y))
8116
- return this._resizeUnalignedShape(id, scale, {
8173
+ this._resizeUnalignedShape(id, scale, {
8117
8174
  ...opts,
8118
8175
  initialBounds,
8119
8176
  scaleOrigin,
@@ -8122,6 +8179,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8122
8179
  isAspectRatioLocked,
8123
8180
  initialShape,
8124
8181
  })
8182
+ return null
8125
8183
  }
8126
8184
 
8127
8185
  const util = this.getShapeUtil(initialShape)
@@ -8134,7 +8192,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8134
8192
  }
8135
8193
  }
8136
8194
 
8137
- let didResize = false
8195
+ let workingShape: TLShape | null = null
8138
8196
 
8139
8197
  if (util.onResize && util.canResize(initialShape)) {
8140
8198
  // get the model changes from the shape util
@@ -8166,7 +8224,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8166
8224
  // need to adjust the shape's x and y points in case the parent has moved since start of resizing
8167
8225
  const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint)
8168
8226
 
8169
- let workingShape = initialShape
8227
+ workingShape = initialShape
8170
8228
  if (!opts.skipStartAndEndCallbacks) {
8171
8229
  workingShape = applyPartialToRecordWithProps(
8172
8230
  initialShape,
@@ -8188,10 +8246,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8188
8246
  }
8189
8247
  )
8190
8248
 
8191
- if (resizedShape) {
8192
- didResize = true
8193
- }
8194
-
8195
8249
  workingShape = applyPartialToRecordWithProps(workingShape, {
8196
8250
  id,
8197
8251
  type: initialShape.type as any,
@@ -8207,40 +8261,47 @@ export class Editor extends EventEmitter<TLEventMap> {
8207
8261
  )
8208
8262
  }
8209
8263
 
8210
- this.updateShapes([workingShape])
8264
+ if (resizedShape) {
8265
+ return workingShape
8266
+ }
8211
8267
  }
8212
8268
 
8213
- if (!didResize) {
8214
- // reposition shape (rather than resizing it) based on where its resized center would be
8269
+ // the shape was not resized by its util, so reposition it (rather than resizing it)
8270
+ // based on where its resized center would be
8215
8271
 
8216
- const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
8217
- // get the model changes from the shape util
8218
- const newPageCenter = this._scalePagePoint(
8219
- initialPageCenter,
8220
- scaleOrigin,
8221
- scale,
8222
- scaleAxisRotation
8223
- )
8272
+ const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
8273
+ // get the model changes from the shape util
8274
+ const newPageCenter = this._scalePagePoint(
8275
+ initialPageCenter,
8276
+ scaleOrigin,
8277
+ scale,
8278
+ scaleAxisRotation
8279
+ )
8224
8280
 
8225
- const initialPageCenterInParentSpace = this.getPointInParentSpace(
8226
- initialShape.id,
8227
- initialPageCenter
8228
- )
8229
- const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8281
+ const initialPageCenterInParentSpace = this.getPointInParentSpace(
8282
+ initialShape.id,
8283
+ initialPageCenter
8284
+ )
8285
+ const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8230
8286
 
8231
- const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
8232
- // apply the changes to the model
8233
- this.updateShapes([
8234
- {
8235
- id,
8236
- type: initialShape.type as any,
8237
- x: initialShape.x + delta.x,
8238
- y: initialShape.y + delta.y,
8239
- },
8240
- ])
8287
+ const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
8288
+
8289
+ if (workingShape) {
8290
+ // the util's onResize ran but returned no change; keep the working update (which may
8291
+ // include changes from onResizeStart / onResizeEnd) and reposition the shape
8292
+ return {
8293
+ ...workingShape,
8294
+ x: initialShape.x + delta.x,
8295
+ y: initialShape.y + delta.y,
8296
+ }
8241
8297
  }
8242
8298
 
8243
- return this
8299
+ return {
8300
+ id,
8301
+ type: initialShape.type as any,
8302
+ x: initialShape.x + delta.x,
8303
+ y: initialShape.y + delta.y,
8304
+ }
8244
8305
  }
8245
8306
 
8246
8307
  /** @internal */
@@ -10120,6 +10181,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10120
10181
 
10121
10182
  if (ids.length === 0) return undefined
10122
10183
 
10184
+ // Text geometry is measured from the loaded font, so the export's bounds - and the
10185
+ // layout of any text within it - depend on the right fonts having loaded. Wait for them
10186
+ // before we measure; otherwise an export taken before fonts finish loading (e.g. right
10187
+ // after the editor mounts) is sized and laid out with fallback-font metrics.
10188
+ await this.fonts.loadRequiredFontsForCurrentPage(this.options.maxFontsToLoadBeforeRender)
10189
+
10123
10190
  return exportToSvg(this, ids, opts)
10124
10191
  }
10125
10192
 
@@ -10768,6 +10835,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10768
10835
  /** @internal */
10769
10836
  private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
10770
10837
 
10838
+ /**
10839
+ * Whether `_selectedShapeIdsAtPointerDown` holds a pre-gesture selection
10840
+ * captured by a `pointer_down` (the touch path) that a following pinch
10841
+ * should restore. False when no pointer_down preceded the pinch (the
10842
+ * Safari trackpad path uses gesture events), in which case `pinch_start`
10843
+ * captures the live selection instead.
10844
+ * @internal
10845
+ */
10846
+ private _didCaptureSelectionAtPointerDown = false
10847
+
10771
10848
  /** @internal */
10772
10849
  private _longPressTimeout = -1 as any
10773
10850
 
@@ -10933,16 +11010,28 @@ export class Editor extends EventEmitter<TLEventMap> {
10933
11010
  if (inputs.getIsPinching()) return
10934
11011
 
10935
11012
  if (!inputs.getIsEditing()) {
10936
- // Always capture the current selection when pinch starts.
10937
- // This ensures Safari (which uses gesture events instead of wheel)
10938
- // doesn't restore a stale selection from an earlier pointer_down.
10939
- this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
11013
+ // If a pointer_down already captured the pre-gesture selection,
11014
+ // keep it: on touch, the first finger's pointer_down can change
11015
+ // the selection before the second finger starts the pinch, and we
11016
+ // want to restore the selection from before that change. When no
11017
+ // pointer_down preceded the pinch (Safari delivers trackpad pinches
11018
+ // as gesture events), capture the live selection now.
11019
+ if (!this._didCaptureSelectionAtPointerDown) {
11020
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
11021
+ }
10940
11022
 
10941
11023
  this._didPinch = true
10942
11024
 
10943
11025
  inputs.setIsPinching(true)
10944
11026
 
10945
11027
  this.interrupt()
11028
+
11029
+ // If the first finger changed the selection, roll it back now rather
11030
+ // than waiting for the pinch to end, so the pre-gesture selection is
11031
+ // what's shown during the pinch.
11032
+ if (this._didCaptureSelectionAtPointerDown) {
11033
+ this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
11034
+ }
10946
11035
  }
10947
11036
 
10948
11037
  this.emit('event', info)
@@ -10994,6 +11083,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10994
11083
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
10995
11084
  this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
10996
11085
  this._selectedShapeIdsAtPointerDown = []
11086
+ this._didCaptureSelectionAtPointerDown = false
10997
11087
 
10998
11088
  if (this._didPinch) {
10999
11089
  this._didPinch = false
@@ -11122,8 +11212,15 @@ export class Editor extends EventEmitter<TLEventMap> {
11122
11212
  }, this.options.longPressDurationMs)
11123
11213
  }
11124
11214
 
11125
- // Save the selected ids at pointer down
11126
- this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11215
+ // Save the selected ids at the start of an interaction so a pinch can
11216
+ // restore the pre-gesture selection. Only capture on the first pointer:
11217
+ // on touch, the second finger's pointer_down arrives after the first
11218
+ // has already changed the selection, and we want the earlier snapshot.
11219
+ // Cleared on pointer_up / pinch_end.
11220
+ if (!this._didCaptureSelectionAtPointerDown) {
11221
+ this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11222
+ this._didCaptureSelectionAtPointerDown = true
11223
+ }
11127
11224
 
11128
11225
  // Firefox bug fix...
11129
11226
  // If it's a left-mouse-click, we store the pointer id for later user
@@ -11136,8 +11233,15 @@ export class Editor extends EventEmitter<TLEventMap> {
11136
11233
  inputs.setIsPointing(true)
11137
11234
  inputs.setIsDragging(false)
11138
11235
 
11139
- // If pen mode is off but we're not already in pen mode, turn that on
11140
- if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
11236
+ // If pen mode is off, turn it on for direct-display pen input only (e.g. Apple
11237
+ // Pencil on an iPad or a Surface Pen on a touchscreen). Indirect desktop tablet
11238
+ // styluses still draw as pens, but should not auto-enable pen mode.
11239
+ if (!isPenMode && info.isPenDirect) {
11240
+ this.updateInstanceState({ isPenMode: true })
11241
+ // Once pen mode is on, touch input is ignored, so we discard the
11242
+ // in-progress touch interaction .
11243
+ this.interrupt()
11244
+ }
11141
11245
 
11142
11246
  // On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser
11143
11247
  if (info.button === STYLUS_ERASER_BUTTON) {
@@ -11243,6 +11347,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11243
11347
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
11244
11348
  this.inputs.setIsRightPointing(false)
11245
11349
  this._selectedShapeIdsAtPointerDown = []
11350
+ this._didCaptureSelectionAtPointerDown = false
11246
11351
  break // fall through to state chart dispatch as right_click
11247
11352
  }
11248
11353
 
@@ -11286,15 +11391,22 @@ export class Editor extends EventEmitter<TLEventMap> {
11286
11391
  // Don't pass right-click panning events to the state chart
11287
11392
  // as it causes unintended shape selection on release
11288
11393
  if (slideSpeed > 0) {
11289
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11394
+ this.slideCamera({
11395
+ speed: slideSpeed,
11396
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11397
+ })
11290
11398
  }
11291
11399
  this._selectedShapeIdsAtPointerDown = []
11400
+ this._didCaptureSelectionAtPointerDown = false
11292
11401
  return this
11293
11402
  }
11294
11403
  }
11295
11404
 
11296
11405
  if (slideSpeed > 0) {
11297
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11406
+ this.slideCamera({
11407
+ speed: slideSpeed,
11408
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11409
+ })
11298
11410
  }
11299
11411
  } else {
11300
11412
  if (info.button === STYLUS_ERASER_BUTTON) {
@@ -11307,6 +11419,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11307
11419
  // Clear the stashed selection so the next pinch captures fresh state.
11308
11420
  // This fixes Safari pinch zoom restoring outdated selections.
11309
11421
  this._selectedShapeIdsAtPointerDown = []
11422
+ this._didCaptureSelectionAtPointerDown = false
11310
11423
 
11311
11424
  break
11312
11425
  }
@@ -11408,6 +11521,22 @@ export class Editor extends EventEmitter<TLEventMap> {
11408
11521
  info.name = 'right_click'
11409
11522
  }
11410
11523
 
11524
+ // The context menu is a select-tool surface: every item acts on the
11525
+ // current selection. Route a right-click through the select tool before
11526
+ // the event reaches the state chart, so the select tool's idle resolves
11527
+ // the selection at the click point (select the shape under the pointer,
11528
+ // or clear it) no matter which tool was active. Without this, opening the
11529
+ // menu from another tool leaves a stale selection that doesn't match the
11530
+ // click (#8828). Guarded on the select tool existing, since the bare
11531
+ // editor can be configured without it.
11532
+ if (
11533
+ info.name === 'right_click' &&
11534
+ this.getCurrentToolId() !== 'select' &&
11535
+ this.getStateDescendant('select')
11536
+ ) {
11537
+ this.setCurrentTool('select')
11538
+ }
11539
+
11411
11540
  // If a left click pointer event, send the event to the click manager.
11412
11541
  const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
11413
11542
  if (info.isPen === isPenMode) {