@tldraw/editor 5.1.0 → 5.2.0-canary.05c017c18b15

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 (138) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +50 -50
  3. package/dist-cjs/index.js +4 -4
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
  6. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
  7. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
  9. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
  10. package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
  11. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
  13. package/dist-cjs/lib/editor/Editor.js +123 -55
  14. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
  16. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  17. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
  18. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  19. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
  20. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  22. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
  24. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
  26. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
  28. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
  30. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
  34. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  35. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  36. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  37. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -0
  38. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  39. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  40. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  41. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  42. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  43. package/dist-cjs/lib/license/LicenseProvider.js +3 -1
  44. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  45. package/dist-cjs/lib/primitives/utils.js +2 -2
  46. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  47. package/dist-cjs/lib/utils/dom.js +5 -3
  48. package/dist-cjs/lib/utils/dom.js.map +2 -2
  49. package/dist-cjs/version.js +3 -3
  50. package/dist-cjs/version.js.map +1 -1
  51. package/dist-esm/index.d.mts +50 -50
  52. package/dist-esm/index.mjs +5 -7
  53. package/dist-esm/index.mjs.map +2 -2
  54. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
  55. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
  56. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
  59. package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
  60. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
  62. package/dist-esm/lib/editor/Editor.mjs +123 -55
  63. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  64. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
  65. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  66. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
  67. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  68. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
  69. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  71. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  73. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  74. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  75. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
  77. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
  79. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
  81. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  83. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  84. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  85. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  86. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -0
  87. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  88. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  89. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  90. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  91. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  92. package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
  93. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/utils.mjs +2 -2
  95. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  96. package/dist-esm/lib/utils/dom.mjs +5 -3
  97. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  98. package/dist-esm/version.mjs +3 -3
  99. package/dist-esm/version.mjs.map +1 -1
  100. package/editor.css +2 -0
  101. package/package.json +8 -8
  102. package/src/index.ts +2 -5
  103. package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
  104. package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
  105. package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
  106. package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
  107. package/src/lib/editor/Editor.ts +172 -70
  108. package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
  109. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  110. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
  111. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  112. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  113. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
  114. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
  115. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
  116. package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
  117. package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
  118. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  119. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
  120. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
  121. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  122. package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
  123. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  124. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  125. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  126. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -0
  127. package/src/lib/editor/tools/StateNode.ts +0 -2
  128. package/src/lib/editor/types/event-types.ts +2 -6
  129. package/src/lib/hooks/usePresence.ts +2 -2
  130. package/src/lib/license/LicenseProvider.tsx +3 -1
  131. package/src/lib/primitives/utils.ts +1 -1
  132. package/src/lib/utils/dom.ts +5 -3
  133. package/src/version.ts +3 -3
  134. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  135. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  136. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  137. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  138. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
@@ -1551,6 +1551,15 @@ export class Editor extends EventEmitter<TLEventMap> {
1551
1551
  return this.history.getMarkIdMatching(idSubstring)
1552
1552
  }
1553
1553
 
1554
+ /**
1555
+ * Whether the editor is currently replaying history (i.e. an undo or redo is being applied).
1556
+ *
1557
+ * @internal
1558
+ */
1559
+ isReplayingHistory(): boolean {
1560
+ return this.history.isReplaying()
1561
+ }
1562
+
1554
1563
  /**
1555
1564
  * Coalesces all changes since the given mark into a single change, removing any intermediate marks.
1556
1565
  *
@@ -3068,8 +3077,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3068
3077
  return baseCamera
3069
3078
  }
3070
3079
 
3071
- private _getFollowingPresence(targetUserId: string | null) {
3072
- const visited = [this.user.getId()]
3080
+ private _getFollowingPresence(targetUserId: TLUserId | null) {
3081
+ const visited = [this.user.getRecordId()]
3073
3082
  const collaborators = this.getCollaborators()
3074
3083
  let leaderPresence = null as null | TLInstancePresence
3075
3084
  while (targetUserId && !visited.includes(targetUserId)) {
@@ -3336,6 +3345,15 @@ export class Editor extends EventEmitter<TLEventMap> {
3336
3345
 
3337
3346
  let { x, y, z = currentCamera.z } = point
3338
3347
 
3348
+ // `requested` kept the caller's focal point (e.g. the cursor) fixed at
3349
+ // zoom `rz`. When `rz` gets clamped, keep that same focal point fixed at
3350
+ // the clamped zoom `z` rather than snapping to the viewport center.
3351
+ const preserveFocalPoint = (current: number, requested: number, rz: number, z: number) => {
3352
+ const cz = currentCamera.z
3353
+ if (rz === cz) return current
3354
+ return current + ((requested - current) * (1 / z - 1 / cz)) / (1 / rz - 1 / cz)
3355
+ }
3356
+
3339
3357
  // If force is true, then we'll set the camera to the point regardless of
3340
3358
  // the camera options, so that we can handle gestures that permit elasticity
3341
3359
  // or decay, or animations that occur while the camera is locked.
@@ -3378,17 +3396,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3378
3396
  }
3379
3397
 
3380
3398
  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
3399
+ // We're trying to zoom out past the minimum zoom level, or in
3400
+ // past the maximum zoom level, so clamp the zoom while keeping
3401
+ // the caller's focal point fixed. Axis constraints below still
3402
+ // apply on top of this.
3403
+ const rz = z
3387
3404
  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
3405
+ x = preserveFocalPoint(currentCamera.x, x, rz, z)
3406
+ y = preserveFocalPoint(currentCamera.y, y, rz, z)
3392
3407
  }
3393
3408
 
3394
3409
  // Calculate available space
@@ -3477,12 +3492,12 @@ export class Editor extends EventEmitter<TLEventMap> {
3477
3492
  }
3478
3493
  }
3479
3494
  } else {
3480
- // constrain the zoom, preserving the center
3495
+ // constrain the zoom, keeping the caller's focal point fixed
3481
3496
  if (z > zoomMax || z < zoomMin) {
3482
- const { x: cx, y: cy, z: cz } = currentCamera
3497
+ const rz = z
3483
3498
  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)
3499
+ x = preserveFocalPoint(currentCamera.x, x, rz, z)
3500
+ y = preserveFocalPoint(currentCamera.y, y, rz, z)
3486
3501
  }
3487
3502
  }
3488
3503
  }
@@ -4023,16 +4038,34 @@ export class Editor extends EventEmitter<TLEventMap> {
4023
4038
 
4024
4039
  this.once('stop-camera-animation', cancel)
4025
4040
 
4041
+ const dirZ = direction.z ?? 0
4042
+
4026
4043
  const moveCamera = (elapsed: number) => {
4027
4044
  const { x: cx, y: cy, z: cz } = this.getCamera()
4028
- const movementVec = Vec.Mul(direction, (currentSpeed * elapsed) / cz)
4045
+
4046
+ // Pan movement from x/y direction
4047
+ const dx = (direction.x * (currentSpeed * elapsed)) / cz
4048
+ const dy = (direction.y * (currentSpeed * elapsed)) / cz
4049
+
4050
+ let newCx = cx + dx
4051
+ let newCy = cy + dy
4052
+ let newCz = cz
4053
+
4054
+ // animate zoom if z direction is passed in
4055
+ if (dirZ !== 0) {
4056
+ newCz = cz * (1 + dirZ * currentSpeed * elapsed)
4057
+ // Adjust x/y to keep the viewport center fixed while zooming
4058
+ const center = this.getViewportScreenCenter()
4059
+ newCx += center.x / newCz - center.x / cz
4060
+ newCy += center.y / newCz - center.y / cz
4061
+ }
4029
4062
 
4030
4063
  // Apply friction
4031
4064
  currentSpeed *= 1 - friction
4032
4065
  if (currentSpeed < speedThreshold) {
4033
4066
  cancel()
4034
4067
  } else {
4035
- this._setCamera(new Vec(cx + movementVec.x, cy + movementVec.y, cz))
4068
+ this._setCamera(new Vec(newCx, newCy, newCz))
4036
4069
  }
4037
4070
  }
4038
4071
 
@@ -4054,7 +4087,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4054
4087
  * @param opts - The camera move options.
4055
4088
  * @public
4056
4089
  */
4057
- zoomToUser(userId: string, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this {
4090
+ zoomToUser(userId: TLUserId, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this {
4058
4091
  const presence = this.getCollaborators().find((c) => c.userId === userId)
4059
4092
 
4060
4093
  if (!presence) return this
@@ -4426,11 +4459,11 @@ export class Editor extends EventEmitter<TLEventMap> {
4426
4459
  *
4427
4460
  * @public
4428
4461
  */
4429
- startFollowingUser(userId: string): this {
4462
+ startFollowingUser(userId: TLUserId): this {
4430
4463
  // if we were already following someone, stop following them
4431
4464
  this.stopFollowingUser()
4432
4465
 
4433
- const thisUserId = this.user.getId()
4466
+ const thisUserId = this.user.getExternalId()
4434
4467
 
4435
4468
  if (!thisUserId) {
4436
4469
  console.warn('You should set the userId for the current instance before following a user')
@@ -8078,32 +8111,53 @@ export class Editor extends EventEmitter<TLEventMap> {
8078
8111
  * @public
8079
8112
  */
8080
8113
  resizeShape(shape: TLShapeId | TLShape, scale: VecLike, opts: TLResizeShapeOptions = {}): this {
8114
+ const partial = this.getResizeShapePartial(shape, scale, opts)
8115
+ if (partial) this.updateShapes([partial])
8116
+ return this
8117
+ }
8118
+
8119
+ /**
8120
+ * Get the update for a resized shape without committing it to the store. Interactions that
8121
+ * resize many shapes at once use this to collect all of the updates and commit them in a
8122
+ * single batch. Returns null when there is nothing to update.
8123
+ *
8124
+ * Shapes that are rotated out of alignment with the scale axis cannot be resized with a
8125
+ * single update; those shapes are resized immediately (as `resizeShape` would do) and null
8126
+ * is returned.
8127
+ *
8128
+ * @internal
8129
+ */
8130
+ getResizeShapePartial(
8131
+ shape: TLShapeId | TLShape,
8132
+ scale: VecLike,
8133
+ opts: TLResizeShapeOptions = {}
8134
+ ): TLShapePartial | null {
8081
8135
  const id = typeof shape === 'string' ? shape : shape.id
8082
- if (this.getIsReadonly()) return this
8136
+ if (this.getIsReadonly()) return null
8083
8137
 
8084
8138
  if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y)
8085
8139
  if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1)
8086
8140
 
8087
8141
  const initialShape = opts.initialShape ?? this.getShape(id)
8088
- if (!initialShape) return this
8142
+ if (!initialShape) return null
8089
8143
 
8090
8144
  const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center
8091
- if (!scaleOrigin) return this
8145
+ if (!scaleOrigin) return null
8092
8146
 
8093
8147
  const pageTransform = opts.initialPageTransform
8094
8148
  ? Mat.Cast(opts.initialPageTransform)
8095
8149
  : this.getShapePageTransform(id)
8096
- if (!pageTransform) return this
8150
+ if (!pageTransform) return null
8097
8151
 
8098
8152
  const pageRotation = pageTransform.rotation()
8099
8153
 
8100
- if (pageRotation == null) return this
8154
+ if (pageRotation == null) return null
8101
8155
 
8102
8156
  const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
8103
8157
 
8104
8158
  const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
8105
8159
 
8106
- if (!initialBounds) return this
8160
+ if (!initialBounds) return null
8107
8161
 
8108
8162
  const isAspectRatioLocked =
8109
8163
  opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape)
@@ -8113,7 +8167,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8113
8167
  // from whichever axis is being scaled the least, to avoid the shape getting bigger
8114
8168
  // than the bounds of the selection
8115
8169
  // const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y))
8116
- return this._resizeUnalignedShape(id, scale, {
8170
+ this._resizeUnalignedShape(id, scale, {
8117
8171
  ...opts,
8118
8172
  initialBounds,
8119
8173
  scaleOrigin,
@@ -8122,6 +8176,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8122
8176
  isAspectRatioLocked,
8123
8177
  initialShape,
8124
8178
  })
8179
+ return null
8125
8180
  }
8126
8181
 
8127
8182
  const util = this.getShapeUtil(initialShape)
@@ -8134,7 +8189,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8134
8189
  }
8135
8190
  }
8136
8191
 
8137
- let didResize = false
8192
+ let workingShape: TLShape | null = null
8138
8193
 
8139
8194
  if (util.onResize && util.canResize(initialShape)) {
8140
8195
  // get the model changes from the shape util
@@ -8166,7 +8221,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8166
8221
  // need to adjust the shape's x and y points in case the parent has moved since start of resizing
8167
8222
  const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint)
8168
8223
 
8169
- let workingShape = initialShape
8224
+ workingShape = initialShape
8170
8225
  if (!opts.skipStartAndEndCallbacks) {
8171
8226
  workingShape = applyPartialToRecordWithProps(
8172
8227
  initialShape,
@@ -8188,10 +8243,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8188
8243
  }
8189
8244
  )
8190
8245
 
8191
- if (resizedShape) {
8192
- didResize = true
8193
- }
8194
-
8195
8246
  workingShape = applyPartialToRecordWithProps(workingShape, {
8196
8247
  id,
8197
8248
  type: initialShape.type as any,
@@ -8207,40 +8258,47 @@ export class Editor extends EventEmitter<TLEventMap> {
8207
8258
  )
8208
8259
  }
8209
8260
 
8210
- this.updateShapes([workingShape])
8261
+ if (resizedShape) {
8262
+ return workingShape
8263
+ }
8211
8264
  }
8212
8265
 
8213
- if (!didResize) {
8214
- // reposition shape (rather than resizing it) based on where its resized center would be
8266
+ // the shape was not resized by its util, so reposition it (rather than resizing it)
8267
+ // based on where its resized center would be
8215
8268
 
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
- )
8269
+ const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
8270
+ // get the model changes from the shape util
8271
+ const newPageCenter = this._scalePagePoint(
8272
+ initialPageCenter,
8273
+ scaleOrigin,
8274
+ scale,
8275
+ scaleAxisRotation
8276
+ )
8224
8277
 
8225
- const initialPageCenterInParentSpace = this.getPointInParentSpace(
8226
- initialShape.id,
8227
- initialPageCenter
8228
- )
8229
- const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8278
+ const initialPageCenterInParentSpace = this.getPointInParentSpace(
8279
+ initialShape.id,
8280
+ initialPageCenter
8281
+ )
8282
+ const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
8230
8283
 
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
- ])
8284
+ const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
8285
+
8286
+ if (workingShape) {
8287
+ // the util's onResize ran but returned no change; keep the working update (which may
8288
+ // include changes from onResizeStart / onResizeEnd) and reposition the shape
8289
+ return {
8290
+ ...workingShape,
8291
+ x: initialShape.x + delta.x,
8292
+ y: initialShape.y + delta.y,
8293
+ }
8241
8294
  }
8242
8295
 
8243
- return this
8296
+ return {
8297
+ id,
8298
+ type: initialShape.type as any,
8299
+ x: initialShape.x + delta.x,
8300
+ y: initialShape.y + delta.y,
8301
+ }
8244
8302
  }
8245
8303
 
8246
8304
  /** @internal */
@@ -10768,6 +10826,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10768
10826
  /** @internal */
10769
10827
  private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
10770
10828
 
10829
+ /**
10830
+ * Whether `_selectedShapeIdsAtPointerDown` holds a pre-gesture selection
10831
+ * captured by a `pointer_down` (the touch path) that a following pinch
10832
+ * should restore. False when no pointer_down preceded the pinch (the
10833
+ * Safari trackpad path uses gesture events), in which case `pinch_start`
10834
+ * captures the live selection instead.
10835
+ * @internal
10836
+ */
10837
+ private _didCaptureSelectionAtPointerDown = false
10838
+
10771
10839
  /** @internal */
10772
10840
  private _longPressTimeout = -1 as any
10773
10841
 
@@ -10933,16 +11001,28 @@ export class Editor extends EventEmitter<TLEventMap> {
10933
11001
  if (inputs.getIsPinching()) return
10934
11002
 
10935
11003
  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]
11004
+ // If a pointer_down already captured the pre-gesture selection,
11005
+ // keep it: on touch, the first finger's pointer_down can change
11006
+ // the selection before the second finger starts the pinch, and we
11007
+ // want to restore the selection from before that change. When no
11008
+ // pointer_down preceded the pinch (Safari delivers trackpad pinches
11009
+ // as gesture events), capture the live selection now.
11010
+ if (!this._didCaptureSelectionAtPointerDown) {
11011
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
11012
+ }
10940
11013
 
10941
11014
  this._didPinch = true
10942
11015
 
10943
11016
  inputs.setIsPinching(true)
10944
11017
 
10945
11018
  this.interrupt()
11019
+
11020
+ // If the first finger changed the selection, roll it back now rather
11021
+ // than waiting for the pinch to end, so the pre-gesture selection is
11022
+ // what's shown during the pinch.
11023
+ if (this._didCaptureSelectionAtPointerDown) {
11024
+ this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
11025
+ }
10946
11026
  }
10947
11027
 
10948
11028
  this.emit('event', info)
@@ -10994,6 +11074,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10994
11074
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
10995
11075
  this.setSelectedShapes(this._selectedShapeIdsAtPointerDown)
10996
11076
  this._selectedShapeIdsAtPointerDown = []
11077
+ this._didCaptureSelectionAtPointerDown = false
10997
11078
 
10998
11079
  if (this._didPinch) {
10999
11080
  this._didPinch = false
@@ -11122,8 +11203,15 @@ export class Editor extends EventEmitter<TLEventMap> {
11122
11203
  }, this.options.longPressDurationMs)
11123
11204
  }
11124
11205
 
11125
- // Save the selected ids at pointer down
11126
- this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11206
+ // Save the selected ids at the start of an interaction so a pinch can
11207
+ // restore the pre-gesture selection. Only capture on the first pointer:
11208
+ // on touch, the second finger's pointer_down arrives after the first
11209
+ // has already changed the selection, and we want the earlier snapshot.
11210
+ // Cleared on pointer_up / pinch_end.
11211
+ if (!this._didCaptureSelectionAtPointerDown) {
11212
+ this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
11213
+ this._didCaptureSelectionAtPointerDown = true
11214
+ }
11127
11215
 
11128
11216
  // Firefox bug fix...
11129
11217
  // If it's a left-mouse-click, we store the pointer id for later user
@@ -11137,7 +11225,12 @@ export class Editor extends EventEmitter<TLEventMap> {
11137
11225
  inputs.setIsDragging(false)
11138
11226
 
11139
11227
  // If pen mode is off but we're not already in pen mode, turn that on
11140
- if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
11228
+ if (!isPenMode && isPen) {
11229
+ this.updateInstanceState({ isPenMode: true })
11230
+ // Once pen mode is on, touch input is ignored, so we discard the
11231
+ // in-progress touch interaction .
11232
+ this.interrupt()
11233
+ }
11141
11234
 
11142
11235
  // On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser
11143
11236
  if (info.button === STYLUS_ERASER_BUTTON) {
@@ -11243,6 +11336,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11243
11336
  if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) {
11244
11337
  this.inputs.setIsRightPointing(false)
11245
11338
  this._selectedShapeIdsAtPointerDown = []
11339
+ this._didCaptureSelectionAtPointerDown = false
11246
11340
  break // fall through to state chart dispatch as right_click
11247
11341
  }
11248
11342
 
@@ -11286,15 +11380,22 @@ export class Editor extends EventEmitter<TLEventMap> {
11286
11380
  // Don't pass right-click panning events to the state chart
11287
11381
  // as it causes unintended shape selection on release
11288
11382
  if (slideSpeed > 0) {
11289
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11383
+ this.slideCamera({
11384
+ speed: slideSpeed,
11385
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11386
+ })
11290
11387
  }
11291
11388
  this._selectedShapeIdsAtPointerDown = []
11389
+ this._didCaptureSelectionAtPointerDown = false
11292
11390
  return this
11293
11391
  }
11294
11392
  }
11295
11393
 
11296
11394
  if (slideSpeed > 0) {
11297
- this.slideCamera({ speed: slideSpeed, direction: slideDirection })
11395
+ this.slideCamera({
11396
+ speed: slideSpeed,
11397
+ direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
11398
+ })
11298
11399
  }
11299
11400
  } else {
11300
11401
  if (info.button === STYLUS_ERASER_BUTTON) {
@@ -11307,6 +11408,7 @@ export class Editor extends EventEmitter<TLEventMap> {
11307
11408
  // Clear the stashed selection so the next pinch captures fresh state.
11308
11409
  // This fixes Safari pinch zoom restoring outdated selections.
11309
11410
  this._selectedShapeIdsAtPointerDown = []
11411
+ this._didCaptureSelectionAtPointerDown = false
11310
11412
 
11311
11413
  break
11312
11414
  }
@@ -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() {