@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
@@ -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
@@ -4426,11 +4458,11 @@ export class Editor extends EventEmitter<TLEventMap> {
4426
4458
  *
4427
4459
  * @public
4428
4460
  */
4429
- startFollowingUser(userId: string): this {
4461
+ startFollowingUser(userId: TLUserId): this {
4430
4462
  // if we were already following someone, stop following them
4431
4463
  this.stopFollowingUser()
4432
4464
 
4433
- const thisUserId = this.user.getId()
4465
+ const thisUserId = this.user.getExternalId()
4434
4466
 
4435
4467
  if (!thisUserId) {
4436
4468
  console.warn('You should set the userId for the current instance before following a user')
@@ -8078,32 +8110,53 @@ export class Editor extends EventEmitter<TLEventMap> {
8078
8110
  * @public
8079
8111
  */
8080
8112
  resizeShape(shape: TLShapeId | TLShape, scale: VecLike, opts: TLResizeShapeOptions = {}): this {
8113
+ const partial = this.getResizeShapePartial(shape, scale, opts)
8114
+ if (partial) this.updateShapes([partial])
8115
+ return this
8116
+ }
8117
+
8118
+ /**
8119
+ * Get the update for a resized shape without committing it to the store. Interactions that
8120
+ * resize many shapes at once use this to collect all of the updates and commit them in a
8121
+ * single batch. Returns null when there is nothing to update.
8122
+ *
8123
+ * Shapes that are rotated out of alignment with the scale axis cannot be resized with a
8124
+ * single update; those shapes are resized immediately (as `resizeShape` would do) and null
8125
+ * is returned.
8126
+ *
8127
+ * @internal
8128
+ */
8129
+ getResizeShapePartial(
8130
+ shape: TLShapeId | TLShape,
8131
+ scale: VecLike,
8132
+ opts: TLResizeShapeOptions = {}
8133
+ ): TLShapePartial | null {
8081
8134
  const id = typeof shape === 'string' ? shape : shape.id
8082
- if (this.getIsReadonly()) return this
8135
+ if (this.getIsReadonly()) return null
8083
8136
 
8084
8137
  if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y)
8085
8138
  if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1)
8086
8139
 
8087
8140
  const initialShape = opts.initialShape ?? this.getShape(id)
8088
- if (!initialShape) return this
8141
+ if (!initialShape) return null
8089
8142
 
8090
8143
  const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center
8091
- if (!scaleOrigin) return this
8144
+ if (!scaleOrigin) return null
8092
8145
 
8093
8146
  const pageTransform = opts.initialPageTransform
8094
8147
  ? Mat.Cast(opts.initialPageTransform)
8095
8148
  : this.getShapePageTransform(id)
8096
- if (!pageTransform) return this
8149
+ if (!pageTransform) return null
8097
8150
 
8098
8151
  const pageRotation = pageTransform.rotation()
8099
8152
 
8100
- if (pageRotation == null) return this
8153
+ if (pageRotation == null) return null
8101
8154
 
8102
8155
  const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
8103
8156
 
8104
8157
  const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
8105
8158
 
8106
- if (!initialBounds) return this
8159
+ if (!initialBounds) return null
8107
8160
 
8108
8161
  const isAspectRatioLocked =
8109
8162
  opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape)
@@ -8113,7 +8166,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8113
8166
  // from whichever axis is being scaled the least, to avoid the shape getting bigger
8114
8167
  // than the bounds of the selection
8115
8168
  // const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y))
8116
- return this._resizeUnalignedShape(id, scale, {
8169
+ this._resizeUnalignedShape(id, scale, {
8117
8170
  ...opts,
8118
8171
  initialBounds,
8119
8172
  scaleOrigin,
@@ -8122,6 +8175,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8122
8175
  isAspectRatioLocked,
8123
8176
  initialShape,
8124
8177
  })
8178
+ return null
8125
8179
  }
8126
8180
 
8127
8181
  const util = this.getShapeUtil(initialShape)
@@ -8134,7 +8188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8134
8188
  }
8135
8189
  }
8136
8190
 
8137
- let didResize = false
8191
+ let workingShape: TLShape | null = null
8138
8192
 
8139
8193
  if (util.onResize && util.canResize(initialShape)) {
8140
8194
  // get the model changes from the shape util
@@ -8166,7 +8220,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8166
8220
  // need to adjust the shape's x and y points in case the parent has moved since start of resizing
8167
8221
  const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint)
8168
8222
 
8169
- let workingShape = initialShape
8223
+ workingShape = initialShape
8170
8224
  if (!opts.skipStartAndEndCallbacks) {
8171
8225
  workingShape = applyPartialToRecordWithProps(
8172
8226
  initialShape,
@@ -8188,10 +8242,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8188
8242
  }
8189
8243
  )
8190
8244
 
8191
- if (resizedShape) {
8192
- didResize = true
8193
- }
8194
-
8195
8245
  workingShape = applyPartialToRecordWithProps(workingShape, {
8196
8246
  id,
8197
8247
  type: initialShape.type as any,
@@ -8207,40 +8257,47 @@ export class Editor extends EventEmitter<TLEventMap> {
8207
8257
  )
8208
8258
  }
8209
8259
 
8210
- this.updateShapes([workingShape])
8260
+ if (resizedShape) {
8261
+ return workingShape
8262
+ }
8211
8263
  }
8212
8264
 
8213
- if (!didResize) {
8214
- // reposition shape (rather than resizing it) based on where its resized center would be
8265
+ // the shape was not resized by its util, so reposition it (rather than resizing it)
8266
+ // based on where its resized center would be
8215
8267
 
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
- )
8268
+ const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
8269
+ // get the model changes from the shape util
8270
+ const newPageCenter = this._scalePagePoint(
8271
+ initialPageCenter,
8272
+ scaleOrigin,
8273
+ scale,
8274
+ scaleAxisRotation
8275
+ )
8224
8276
 
8225
- const initialPageCenterInParentSpace = this.getPointInParentSpace(
8226
- initialShape.id,
8227
- initialPageCenter
8228
- )
8229
- const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8277
+ const initialPageCenterInParentSpace = this.getPointInParentSpace(
8278
+ initialShape.id,
8279
+ initialPageCenter
8280
+ )
8281
+ const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8230
8282
 
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
- ])
8283
+ const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
8284
+
8285
+ if (workingShape) {
8286
+ // the util's onResize ran but returned no change; keep the working update (which may
8287
+ // include changes from onResizeStart / onResizeEnd) and reposition the shape
8288
+ return {
8289
+ ...workingShape,
8290
+ x: initialShape.x + delta.x,
8291
+ y: initialShape.y + delta.y,
8292
+ }
8241
8293
  }
8242
8294
 
8243
- return this
8295
+ return {
8296
+ id,
8297
+ type: initialShape.type as any,
8298
+ x: initialShape.x + delta.x,
8299
+ y: initialShape.y + delta.y,
8300
+ }
8244
8301
  }
8245
8302
 
8246
8303
  /** @internal */
@@ -10768,6 +10825,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10768
10825
  /** @internal */
10769
10826
  private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
10770
10827
 
10828
+ /**
10829
+ * Whether `_selectedShapeIdsAtPointerDown` holds a pre-gesture selection
10830
+ * captured by a `pointer_down` (the touch path) that a following pinch
10831
+ * should restore. False when no pointer_down preceded the pinch (the
10832
+ * Safari trackpad path uses gesture events), in which case `pinch_start`
10833
+ * captures the live selection instead.
10834
+ * @internal
10835
+ */
10836
+ private _didCaptureSelectionAtPointerDown = false
10837
+
10771
10838
  /** @internal */
10772
10839
  private _longPressTimeout = -1 as any
10773
10840
 
@@ -10933,16 +11000,28 @@ export class Editor extends EventEmitter<TLEventMap> {
10933
11000
  if (inputs.getIsPinching()) return
10934
11001
 
10935
11002
  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]
11003
+ // If a pointer_down already captured the pre-gesture selection,
11004
+ // keep it: on touch, the first finger's pointer_down can change
11005
+ // the selection before the second finger starts the pinch, and we
11006
+ // want to restore the selection from before that change. When no
11007
+ // pointer_down preceded the pinch (Safari delivers trackpad pinches
11008
+ // as gesture events), capture the live selection now.
11009
+ if (!this._didCaptureSelectionAtPointerDown) {
11010
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
11011
+ }
10940
11012
 
10941
11013
  this._didPinch = true
10942
11014
 
10943
11015
  inputs.setIsPinching(true)
10944
11016
 
10945
11017
  this.interrupt()
11018
+
11019
+ // If the first finger changed the selection, roll it back now rather
11020
+ // than waiting for the pinch to end, so the pre-gesture selection is
11021
+ // what's shown during the pinch.
11022
+ if (this._didCaptureSelectionAtPointerDown) {
11023
+ this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
11024
+ }
10946
11025
  }
10947
11026
 
10948
11027
  this.emit('event', info)
@@ -10994,6 +11073,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10994
11073
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
10995
11074
  this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
10996
11075
  this._selectedShapeIdsAtPointerDown = []
11076
+ this._didCaptureSelectionAtPointerDown = false
10997
11077
 
10998
11078
  if (this._didPinch) {
10999
11079
  this._didPinch = false
@@ -11122,8 +11202,15 @@ export class Editor extends EventEmitter<TLEventMap> {
11122
11202
  }, this.options.longPressDurationMs)
11123
11203
  }
11124
11204
 
11125
- // Save the selected ids at pointer down
11126
- this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11205
+ // Save the selected ids at the start of an interaction so a pinch can
11206
+ // restore the pre-gesture selection. Only capture on the first pointer:
11207
+ // on touch, the second finger's pointer_down arrives after the first
11208
+ // has already changed the selection, and we want the earlier snapshot.
11209
+ // Cleared on pointer_up / pinch_end.
11210
+ if (!this._didCaptureSelectionAtPointerDown) {
11211
+ this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11212
+ this._didCaptureSelectionAtPointerDown = true
11213
+ }
11127
11214
 
11128
11215
  // Firefox bug fix...
11129
11216
  // If it's a left-mouse-click, we store the pointer id for later user
@@ -11248,6 +11335,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11248
11335
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
11249
11336
  this.inputs.setIsRightPointing(false)
11250
11337
  this._selectedShapeIdsAtPointerDown = []
11338
+ this._didCaptureSelectionAtPointerDown = false
11251
11339
  break // fall through to state chart dispatch as right_click
11252
11340
  }
11253
11341
 
@@ -11291,15 +11379,22 @@ export class Editor extends EventEmitter<TLEventMap> {
11291
11379
  // Don't pass right-click panning events to the state chart
11292
11380
  // as it causes unintended shape selection on release
11293
11381
  if (slideSpeed > 0) {
11294
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11382
+ this.slideCamera({
11383
+ speed: slideSpeed,
11384
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11385
+ })
11295
11386
  }
11296
11387
  this._selectedShapeIdsAtPointerDown = []
11388
+ this._didCaptureSelectionAtPointerDown = false
11297
11389
  return this
11298
11390
  }
11299
11391
  }
11300
11392
 
11301
11393
  if (slideSpeed > 0) {
11302
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11394
+ this.slideCamera({
11395
+ speed: slideSpeed,
11396
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11397
+ })
11303
11398
  }
11304
11399
  } else {
11305
11400
  if (info.button === STYLUS_ERASER_BUTTON) {
@@ -11312,6 +11407,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11312
11407
  // Clear the stashed selection so the next pinch captures fresh state.
11313
11408
  // This fixes Safari pinch zoom restoring outdated selections.
11314
11409
  this._selectedShapeIdsAtPointerDown = []
11410
+ this._didCaptureSelectionAtPointerDown = false
11315
11411
 
11316
11412
  break
11317
11413
  }
@@ -29,7 +29,7 @@ function fromScratch(bindingsQuery: Computed<TLBinding[], unknown>) {
29
29
  return shapesToBindings
30
30
  }
31
31
 
32
- export const bindingsIndex = (editor: Editor): Computed<TLBindingsIndex> => {
32
+ export function bindingsIndex(editor: Editor): Computed<TLBindingsIndex> {
33
33
  const { store } = editor
34
34
  const bindingsHistory = store.query.filterHistory('binding')
35
35
  const bindingsQuery = store.query.records('binding')
@@ -22,7 +22,7 @@ function fromScratch(
22
22
  return result
23
23
  }
24
24
 
25
- export const parentsToChildren = (store: TLStore) => {
25
+ export function parentsToChildren(store: TLStore) {
26
26
  const shapeIdsQuery = store.query.ids<'shape'>('shape')
27
27
  const shapeHistory = store.query.filterHistory('shape')
28
28
 
@@ -33,7 +33,7 @@ const isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolea
33
33
  * @param store - The tldraw store.
34
34
  * @param getCurrentPageId - A function that returns the current page id.
35
35
  */
36
- export const deriveShapeIdsInCurrentPage = (store: TLStore, getCurrentPageId: () => TLPageId) => {
36
+ export function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: () => TLPageId) {
37
37
  const shapesIndex = store.query.ids('shape')
38
38
  let lastPageId: null | TLPageId = null
39
39
  function fromScratch() {