@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.4.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 (249) hide show
  1. package/dist-cjs/index.d.ts +114 -47
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
  9. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
  12. package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/config/createTLStore.js +1 -1
  15. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  16. package/dist-cjs/lib/editor/Editor.js +181 -294
  17. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  18. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  19. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  23. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  25. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  27. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  29. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  31. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  33. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  35. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  37. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  39. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  41. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  42. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  43. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  44. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  45. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  46. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  47. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  48. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  49. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  50. package/dist-cjs/lib/globals/editors.js +57 -0
  51. package/dist-cjs/lib/globals/editors.js.map +7 -0
  52. package/dist-cjs/lib/globals/menus.js +9 -4
  53. package/dist-cjs/lib/globals/menus.js.map +2 -2
  54. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  55. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  56. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  57. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  58. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  59. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  60. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  61. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  63. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  64. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  65. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  66. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  67. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  68. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  69. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  70. package/dist-cjs/lib/primitives/Box.js +2 -1
  71. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  72. package/dist-cjs/lib/primitives/Vec.js +4 -3
  73. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  74. package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
  75. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  76. package/dist-cjs/lib/primitives/utils.js +2 -1
  77. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  78. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  79. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  80. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  81. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  82. package/dist-cjs/lib/utils/rotation.js +1 -9
  83. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  84. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  85. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  86. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  87. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  88. package/dist-cjs/version.js +3 -3
  89. package/dist-cjs/version.js.map +1 -1
  90. package/dist-esm/index.d.mts +114 -47
  91. package/dist-esm/index.mjs +9 -2
  92. package/dist-esm/index.mjs.map +2 -2
  93. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  94. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  95. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  96. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  97. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  98. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  99. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  100. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  101. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  102. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  103. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  104. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  105. package/dist-esm/lib/editor/Editor.mjs +189 -295
  106. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  107. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  108. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  109. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  110. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  111. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  112. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  113. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  114. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  115. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  116. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  117. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  118. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  119. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  120. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  121. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  122. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  123. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  124. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  126. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  128. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  130. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  131. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  132. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  133. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  134. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  135. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  136. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  137. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  138. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  139. package/dist-esm/lib/globals/editors.mjs +37 -0
  140. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  141. package/dist-esm/lib/globals/menus.mjs +9 -4
  142. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  143. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  144. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  145. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  146. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  147. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  148. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  149. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  150. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  151. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  152. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  153. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  154. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  155. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  156. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  157. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  158. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  159. package/dist-esm/lib/primitives/Box.mjs +2 -1
  160. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  161. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  162. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  163. package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
  164. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  165. package/dist-esm/lib/primitives/utils.mjs +2 -1
  166. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  167. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  168. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  169. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  170. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  171. package/dist-esm/lib/utils/rotation.mjs +1 -9
  172. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  173. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  174. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  175. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  176. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  177. package/dist-esm/version.mjs +3 -3
  178. package/dist-esm/version.mjs.map +1 -1
  179. package/package.json +13 -7
  180. package/src/index.ts +6 -1
  181. package/src/lib/TldrawEditor.tsx +16 -4
  182. package/src/lib/components/MenuClickCapture.tsx +19 -18
  183. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  184. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  185. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  186. package/src/lib/config/TLUserPreferences.ts +8 -4
  187. package/src/lib/config/createTLStore.ts +1 -1
  188. package/src/lib/editor/Editor.ts +266 -383
  189. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  190. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  191. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  192. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  193. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  194. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  195. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  196. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  197. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  198. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  199. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  200. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  201. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  202. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  203. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  204. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  205. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  206. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  207. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  208. package/src/lib/editor/tools/StateNode.ts +12 -2
  209. package/src/lib/globals/editors.test.ts +76 -0
  210. package/src/lib/globals/editors.ts +53 -0
  211. package/src/lib/globals/menus.test.ts +33 -0
  212. package/src/lib/globals/menus.ts +11 -4
  213. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  214. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  215. package/src/lib/hooks/useLocalStore.ts +16 -8
  216. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  217. package/src/lib/hooks/usePeerIds.ts +2 -1
  218. package/src/lib/license/LicenseManager.test.ts +160 -0
  219. package/src/lib/license/LicenseManager.ts +13 -4
  220. package/src/lib/license/LicenseProvider.tsx +24 -3
  221. package/src/lib/license/useLicenseManagerState.ts +8 -6
  222. package/src/lib/primitives/Box.test.ts +9 -1
  223. package/src/lib/primitives/Box.ts +3 -1
  224. package/src/lib/primitives/Vec.test.ts +10 -0
  225. package/src/lib/primitives/Vec.ts +4 -4
  226. package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
  227. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
  228. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  229. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  230. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  231. package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
  232. package/src/lib/primitives/geometry/Group2d.ts +63 -27
  233. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  234. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  235. package/src/lib/primitives/utils.test.ts +10 -1
  236. package/src/lib/primitives/utils.ts +4 -1
  237. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  238. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  239. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  240. package/src/lib/utils/deepLinks.test.ts +15 -0
  241. package/src/lib/utils/deepLinks.ts +7 -1
  242. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  243. package/src/lib/utils/normalizeWheel.ts +6 -0
  244. package/src/lib/utils/rotation.ts +4 -12
  245. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  246. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  247. package/src/lib/utils/sync/alerts.ts +1 -1
  248. package/src/version.ts +3 -3
  249. package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
@@ -72,6 +72,7 @@ import {
72
72
  Result,
73
73
  ZERO_INDEX_KEY,
74
74
  annotateError,
75
+ areArraysShallowEqual,
75
76
  assert,
76
77
  assertExists,
77
78
  bind,
@@ -118,8 +119,10 @@ import {
118
119
  import { getOwnerWindow } from '../exports/domUtils'
119
120
  import { exportToSvg } from '../exports/exportToSvg'
120
121
  import { getSvgAsImageWithOptions, trimSvgToContent } from '../exports/getSvgAsImage'
122
+ import { registerMountedEditor, unregisterMountedEditor } from '../globals/editors'
121
123
  import { tlmenus } from '../globals/menus'
122
124
  import { tltime } from '../globals/time'
125
+ import { LicenseManager } from '../license/LicenseManager'
123
126
  import { TldrawOptions, defaultTldrawOptions } from '../options'
124
127
  import { Box, BoxLike } from '../primitives/Box'
125
128
  import { EASINGS } from '../primitives/easings'
@@ -127,7 +130,13 @@ import { Geometry2d } from '../primitives/geometry/Geometry2d'
127
130
  import { Group2d } from '../primitives/geometry/Group2d'
128
131
  import { intersectPolygonPolygon } from '../primitives/intersect'
129
132
  import { Mat, MatLike } from '../primitives/Mat'
130
- import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
133
+ import {
134
+ HALF_PI,
135
+ approximately,
136
+ areAnglesCompatible,
137
+ clamp,
138
+ pointInPolygon,
139
+ } from '../primitives/utils'
131
140
  import { Vec, VecLike } from '../primitives/Vec'
132
141
  import { areShapesContentEqual } from '../utils/areShapesContentEqual'
133
142
  import { dataUrlToFile } from '../utils/assets'
@@ -185,6 +194,7 @@ import { TLHistoryBatchOptions } from './types/history-types'
185
194
  import {
186
195
  OptionalKeys,
187
196
  RequiredKeys,
197
+ TLCameraConstraints,
188
198
  TLCameraMoveOptions,
189
199
  TLCameraOptions,
190
200
  TLGetShapeAtPointOptions,
@@ -543,6 +553,14 @@ export class Editor extends EventEmitter<TLEventMap> {
543
553
  nextPageState.editingShapeId = null
544
554
  }
545
555
 
556
+ if (
557
+ prevPageState.croppingShapeId &&
558
+ shapesNoLongerInPage.has(prevPageState.croppingShapeId)
559
+ ) {
560
+ if (!nextPageState) nextPageState = { ...prevPageState }
561
+ nextPageState.croppingShapeId = null
562
+ }
563
+
546
564
  const hintingShapeIds = prevPageState.hintingShapeIds.filter(
547
565
  (id) => !shapesNoLongerInPage.has(id)
548
566
  )
@@ -828,10 +846,11 @@ export class Editor extends EventEmitter<TLEventMap> {
828
846
 
829
847
  if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
830
848
  // ensure that descendants and ancestors are not selected at the same time
849
+ const selectedShapeIds = new Set(next.selectedShapeIds)
831
850
  const filtered = next.selectedShapeIds.filter((id) => {
832
851
  let parentId = this.getShape(id)?.parentId
833
852
  while (isShapeId(parentId)) {
834
- if (next.selectedShapeIds.includes(parentId)) {
853
+ if (selectedShapeIds.has(parentId)) {
835
854
  return false
836
855
  }
837
856
  parentId = this.getShape(parentId)?.parentId
@@ -918,10 +937,12 @@ export class Editor extends EventEmitter<TLEventMap> {
918
937
 
919
938
  this.on('mount', () => {
920
939
  this._isMounted.set(true)
940
+ registerMountedEditor(this)
921
941
  })
922
942
 
923
943
  this.on('unmount', () => {
924
944
  this._isMounted.set(false)
945
+ unregisterMountedEditor(this)
925
946
  })
926
947
 
927
948
  this.timers.requestAnimationFrame(() => {
@@ -934,7 +955,11 @@ export class Editor extends EventEmitter<TLEventMap> {
934
955
  const mode = this.store.props.collaboration.mode
935
956
  this.disposables.add(
936
957
  react('update collaboration mode', () => {
937
- this.store.put([{ ...this.getInstanceState(), isReadonly: mode.get() === 'readonly' }])
958
+ const isReadonly = mode.get() === 'readonly'
959
+ // only track `mode`, and keep the sync out of the user's undo history
960
+ unsafe__withoutCapture(() =>
961
+ this._updateInstanceState({ isReadonly }, { history: 'ignore' })
962
+ )
938
963
  })
939
964
  )
940
965
  }
@@ -973,6 +998,15 @@ export class Editor extends EventEmitter<TLEventMap> {
973
998
 
974
999
  readonly options: TldrawOptions
975
1000
 
1001
+ /**
1002
+ * The license manager whose feature flags apply to this editor. Assigned by `<TldrawEditor />`
1003
+ * when it creates the editor, so that UI rendered outside the editor tree can resolve the
1004
+ * editor's license through the editor instance. Undefined for editors created directly.
1005
+ *
1006
+ * @internal
1007
+ */
1008
+ licenseManager?: LicenseManager
1009
+
976
1010
  readonly contextId = uniqueId()
977
1011
 
978
1012
  /**
@@ -1209,6 +1243,9 @@ export class Editor extends EventEmitter<TLEventMap> {
1209
1243
  if (this._isMounted.get()) {
1210
1244
  this.emit('unmount')
1211
1245
  }
1246
+ // The unmount listener above normally handles this; unregister again in case a listener
1247
+ // threw or was removed, so a disposed editor never lingers in `tleditors`.
1248
+ unregisterMountedEditor(this)
1212
1249
 
1213
1250
  // Stop any in-progress camera animations and following before
1214
1251
  // running disposables, so their cleanup listeners fire first
@@ -1374,7 +1411,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1374
1411
  getShapeUtil(arg: string | { type: string }) {
1375
1412
  const type = typeof arg === 'string' ? arg : arg.type
1376
1413
  const shapeUtil = getOwnProperty(this.shapeUtils, type)
1377
- assert(shapeUtil, `No shape util found for type "${type}"`)
1414
+ // hot path: avoid building the message (and the assert wrapper) on every successful call
1415
+ if (!shapeUtil) throw new Error(`No shape util found for type "${type}"`)
1378
1416
  return shapeUtil
1379
1417
  }
1380
1418
 
@@ -2451,9 +2489,9 @@ export class Editor extends EventEmitter<TLEventMap> {
2451
2489
  const selectedShapes = this.getSelectedShapes()
2452
2490
  if (!selectedShapes.length) return
2453
2491
  const selectedShape = selectedShapes[0]
2454
- const children = this.getSortedChildIdsForParent(selectedShape.id)
2455
- .map((id) => this.getShape(id))
2456
- .filter((i) => i) as TLShape[]
2492
+ const children = compact(
2493
+ this.getSortedChildIdsForParent(selectedShape.id).map((id) => this.getShape(id))
2494
+ )
2457
2495
  const sortedChildren = this._getShapesInReadingOrder(children)
2458
2496
  if (sortedChildren.length === 0) return
2459
2497
  this._selectShapesAndZoom([sortedChildren[0].id])
@@ -2554,20 +2592,14 @@ export class Editor extends EventEmitter<TLEventMap> {
2554
2592
  * @internal
2555
2593
  */
2556
2594
  getShapesSharedRotation(shapeIds: TLShapeId[]) {
2557
- let foundFirst = false // annoying but we can't use an i===0 check because we need to skip over undefineds
2558
2595
  let rotation = 0
2559
2596
  for (let i = 0, n = shapeIds.length; i < n; i++) {
2560
- const pageTransform = this.getShapePageTransform(shapeIds[i])
2561
- if (!pageTransform) continue
2562
- if (foundFirst) {
2563
- if (pageTransform.rotation() !== rotation) {
2564
- // There are at least 2 different rotations, so the common rotation is zero
2565
- return 0
2566
- }
2567
- } else {
2568
- // First rotation found
2569
- foundFirst = true
2570
- rotation = pageTransform.rotation()
2597
+ const pageRotation = this.getShapePageTransform(shapeIds[i]).rotation()
2598
+ if (i === 0) {
2599
+ rotation = pageRotation
2600
+ } else if (pageRotation !== rotation) {
2601
+ // There are at least 2 different rotations, so the common rotation is zero
2602
+ return 0
2571
2603
  }
2572
2604
  }
2573
2605
 
@@ -2599,7 +2631,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2599
2631
 
2600
2632
  if (shapeIds.length === 1) {
2601
2633
  const bounds = this.getShapeGeometry(shapeIds[0]).bounds.clone()
2602
- const pageTransform = this.getShapePageTransform(shapeIds[0])!
2634
+ const pageTransform = this.getShapePageTransform(shapeIds[0])
2603
2635
  bounds.point = pageTransform.applyToPoint(bounds.point)
2604
2636
  return bounds
2605
2637
  }
@@ -2607,11 +2639,9 @@ export class Editor extends EventEmitter<TLEventMap> {
2607
2639
  // need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box
2608
2640
  const boxFromRotatedVertices = Box.FromPoints(
2609
2641
  shapeIds
2610
- .flatMap((id) => {
2611
- const pageTransform = this.getShapePageTransform(id)
2612
- if (!pageTransform) return []
2613
- return pageTransform.applyToPoints(this.getShapeGeometry(id).bounds.corners)
2614
- })
2642
+ .flatMap((id) =>
2643
+ this.getShapePageTransform(id).applyToPoints(this.getShapeGeometry(id).bounds.corners)
2644
+ )
2615
2645
  .map((p) => p.rot(-selectionRotation))
2616
2646
  )
2617
2647
  // now position box so that it's top-left corner is in the right place
@@ -2638,9 +2668,16 @@ export class Editor extends EventEmitter<TLEventMap> {
2638
2668
  @computed getSelectionRotatedScreenBounds(): Box | undefined {
2639
2669
  const bounds = this.getSelectionRotatedPageBounds()
2640
2670
  if (!bounds) return undefined
2641
- const { x, y } = this.pageToScreen(bounds.point)
2642
- const zoom = this.getZoomLevel()
2643
- return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
2671
+ // Don't use pageToScreen here: it reads the screen bounds without capturing them, so this
2672
+ // computed would never invalidate when the container moves
2673
+ const screenBounds = this.getViewportScreenBounds()
2674
+ const { x: cx, y: cy, z: zoom } = this.getCamera()
2675
+ return new Box(
2676
+ (bounds.x + cx) * zoom + screenBounds.x,
2677
+ (bounds.y + cy) * zoom + screenBounds.y,
2678
+ bounds.width * zoom,
2679
+ bounds.height * zoom
2680
+ )
2644
2681
  }
2645
2682
 
2646
2683
  // Focus Group
@@ -2780,57 +2817,38 @@ export class Editor extends EventEmitter<TLEventMap> {
2780
2817
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2781
2818
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2782
2819
 
2783
- if (!id) {
2784
- // setting the editing shape to null
2820
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2821
+ if (id && !this.canEditShape(id)) return this
2822
+
2823
+ this.run(() => {
2824
+ // Clean up the previous editing shape. This runs outside the history-ignored batch below,
2825
+ // otherwise document changes made by onEditEnd (e.g. deleting an empty text shape) are
2826
+ // never recorded and leave a phantom undo entry whose redo resurrects the shape.
2827
+ const prevEditingShapeId = this.getEditingShapeId()
2828
+ if (prevEditingShapeId) {
2829
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2830
+ if (prevEditingShape) {
2831
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2832
+ }
2833
+ }
2834
+
2785
2835
  this.run(
2786
2836
  () => {
2787
- // Clean up the previous editing shape
2788
- const prevEditingShapeId = this.getEditingShapeId()
2789
- if (prevEditingShapeId) {
2790
- const prevEditingShape = this.getShape(prevEditingShapeId)
2791
- if (prevEditingShape) {
2792
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2793
- }
2794
- }
2795
-
2796
2837
  // Clean up the editing shape state and rich text editor
2797
2838
  this._updateCurrentPageState({ editingShapeId: null })
2798
2839
  this._currentRichTextEditor.set(null)
2799
- },
2800
- { history: 'ignore' }
2801
- )
2802
2840
 
2803
- return this
2804
- }
2841
+ if (!id) return
2805
2842
 
2806
- // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2807
- if (!this.canEditShape(id)) return this
2843
+ this.select(id)
2844
+ this._updateCurrentPageState({ editingShapeId: id })
2808
2845
 
2809
- // id was provided and the next editing shape is editable, so set the rich text editor to null
2810
- this.run(
2811
- () => {
2812
- // Clean up the previous editing shape
2813
- const prevEditingShapeId = this.getEditingShapeId()
2814
- if (prevEditingShapeId) {
2815
- const prevEditingShape = this.getShape(prevEditingShapeId)
2816
- if (prevEditingShape) {
2817
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2818
- }
2819
- }
2820
-
2821
- // Clean up the editing shape state and rich text editor
2822
- this._updateCurrentPageState({ editingShapeId: null })
2823
- this._currentRichTextEditor.set(null)
2824
-
2825
- // Set the new editing shape
2826
- this.select(id)
2827
- this._updateCurrentPageState({ editingShapeId: id })
2828
-
2829
- const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2830
- this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2831
- },
2832
- { history: 'ignore' }
2833
- )
2846
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2847
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2848
+ },
2849
+ { history: 'ignore' }
2850
+ )
2851
+ })
2834
2852
 
2835
2853
  return this
2836
2854
  }
@@ -2995,26 +3013,17 @@ export class Editor extends EventEmitter<TLEventMap> {
2995
3013
  * @public
2996
3014
  */
2997
3015
  setErasingShapes(shapes: TLShapeId[] | TLShape[]): this {
3016
+ // copy before sorting: the caller may pass a store-owned (frozen) array
2998
3017
  const ids =
2999
3018
  typeof shapes[0] === 'string'
3000
- ? (shapes as TLShapeId[])
3019
+ ? (shapes as TLShapeId[]).slice()
3001
3020
  : (shapes as TLShape[]).map((shape) => shape.id)
3002
3021
  ids.sort() // sort the incoming ids
3003
3022
  const erasingShapeIds = this.getErasingShapeIds()
3004
3023
  this.run(
3005
3024
  () => {
3006
- if (ids.length === erasingShapeIds.length) {
3007
- // if the new ids are the same length as the current ids, they might be the same.
3008
- // presuming the current ids are also sorted, check each item to see if it's the same;
3009
- // if we find any unequal, then we know the new ids are different.
3010
- for (let i = 0; i < ids.length; i++) {
3011
- if (ids[i] !== erasingShapeIds[i]) {
3012
- this._updateCurrentPageState({ erasingShapeIds: ids })
3013
- break
3014
- }
3015
- }
3016
- } else {
3017
- // if the ids are a different length, then we know they're different.
3025
+ // the current ids are also sorted, so a shallow comparison tells us whether they changed
3026
+ if (!areArraysShallowEqual(ids, erasingShapeIds)) {
3018
3027
  this._updateCurrentPageState({ erasingShapeIds: ids })
3019
3028
  }
3020
3029
  },
@@ -3051,6 +3060,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3051
3060
  if (!_shape) return false
3052
3061
  const util = this.getShapeUtil(_shape)
3053
3062
  if (!util.canCrop(_shape)) return false
3063
+ if (this.getIsReadonly()) return false
3054
3064
  if (this.isShapeOrAncestorLocked(_shape)) return false
3055
3065
  return true
3056
3066
  }
@@ -3130,11 +3140,13 @@ export class Editor extends EventEmitter<TLEventMap> {
3130
3140
  const collaborators = this.getCollaborators()
3131
3141
  let leaderPresence = null as null | TLInstancePresence
3132
3142
  while (targetUserId && !visited.includes(targetUserId)) {
3133
- leaderPresence = collaborators.find((c) => c.userId === targetUserId) ?? null
3134
- targetUserId = leaderPresence?.followingUserId ?? null
3135
- if (leaderPresence) {
3136
- visited.push(leaderPresence.userId)
3137
- }
3143
+ const nextPresence = collaborators.find((c) => c.userId === targetUserId)
3144
+ // Stop at the last resolvable presence, otherwise a leader whose own leader has left
3145
+ // (or whose presence hasn't arrived yet) can't be followed at all
3146
+ if (!nextPresence) break
3147
+ leaderPresence = nextPresence
3148
+ targetUserId = nextPresence.followingUserId ?? null
3149
+ visited.push(nextPresence.userId)
3138
3150
  }
3139
3151
  return leaderPresence
3140
3152
  }
@@ -3252,44 +3264,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3252
3264
  *
3253
3265
  * @public */
3254
3266
  getInitialZoom() {
3255
- const cameraOptions = this.getCameraOptions()
3256
- // If no camera constraints are provided, the default zoom is 100%
3257
- if (!cameraOptions.constraints) return 1
3258
-
3259
- // When defaultZoom is default, the default zoom is 100%
3260
- if (cameraOptions.constraints.initialZoom === 'default') return 1
3261
-
3262
- const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
3263
-
3264
- switch (cameraOptions.constraints.initialZoom) {
3265
- case 'fit-min': {
3266
- return Math.max(zx, zy)
3267
- }
3268
- case 'fit-max': {
3269
- return Math.min(zx, zy)
3270
- }
3271
- case 'fit-x': {
3272
- return zx
3273
- }
3274
- case 'fit-y': {
3275
- return zy
3276
- }
3277
- case 'fit-min-100': {
3278
- return Math.min(1, Math.max(zx, zy))
3279
- }
3280
- case 'fit-max-100': {
3281
- return Math.min(1, Math.min(zx, zy))
3282
- }
3283
- case 'fit-x-100': {
3284
- return Math.min(1, zx)
3285
- }
3286
- case 'fit-y-100': {
3287
- return Math.min(1, zy)
3288
- }
3289
- default: {
3290
- throw exhaustiveSwitchError(cameraOptions.constraints.initialZoom)
3291
- }
3292
- }
3267
+ return this._getFitZoom(this.getCameraOptions().constraints?.initialZoom ?? 'default')
3293
3268
  }
3294
3269
 
3295
3270
  /**
@@ -3302,16 +3277,16 @@ export class Editor extends EventEmitter<TLEventMap> {
3302
3277
  *
3303
3278
  * @public */
3304
3279
  getBaseZoom() {
3305
- const cameraOptions = this.getCameraOptions()
3306
- // If no camera constraints are provided, the default zoom is 100%
3307
- if (!cameraOptions.constraints) return 1
3280
+ return this._getFitZoom(this.getCameraOptions().constraints?.baseZoom ?? 'default')
3281
+ }
3308
3282
 
3309
- // When defaultZoom is default, the default zoom is 100%
3310
- if (cameraOptions.constraints.baseZoom === 'default') return 1
3283
+ private _getFitZoom(fit: TLCameraConstraints['initialZoom']): number {
3284
+ const cameraOptions = this.getCameraOptions()
3285
+ if (!cameraOptions.constraints || fit === 'default') return 1
3311
3286
 
3312
3287
  const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
3313
3288
 
3314
- switch (cameraOptions.constraints.baseZoom) {
3289
+ switch (fit) {
3315
3290
  case 'fit-min': {
3316
3291
  return Math.max(zx, zy)
3317
3292
  }
@@ -3337,7 +3312,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3337
3312
  return Math.min(1, zy)
3338
3313
  }
3339
3314
  default: {
3340
- throw exhaustiveSwitchError(cameraOptions.constraints.baseZoom)
3315
+ throw exhaustiveSwitchError(fit)
3341
3316
  }
3342
3317
  }
3343
3318
  }
@@ -3374,7 +3349,9 @@ export class Editor extends EventEmitter<TLEventMap> {
3374
3349
  ...this._cameraOptions.__unsafe__getWithoutCapture(),
3375
3350
  ...opts,
3376
3351
  })
3377
- if (next.zoomSteps?.length < 1) next.zoomSteps = [1]
3352
+ // `undefined < 1` is false, so an explicit `zoomSteps: undefined` would otherwise get through
3353
+ // and make every later camera move throw
3354
+ if (!next.zoomSteps || next.zoomSteps.length < 1) next.zoomSteps = [1]
3378
3355
  this._cameraOptions.set(next)
3379
3356
  this.setCamera(this.getCamera())
3380
3357
  return this
@@ -3420,8 +3397,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3420
3397
  const { constraints } = cameraOptions
3421
3398
 
3422
3399
  // Clamp padding to half the viewport size on either dimension
3423
- const py = Math.min(constraints.padding.y, vsb.w / 2)
3424
- const px = Math.min(constraints.padding.x, vsb.h / 2)
3400
+ const px = Math.min(constraints.padding.x, vsb.w / 2)
3401
+ const py = Math.min(constraints.padding.y, vsb.h / 2)
3425
3402
 
3426
3403
  // Expand the bounds by the padding
3427
3404
  const bounds = Box.From(cameraOptions.constraints.bounds)
@@ -3490,14 +3467,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3490
3467
  }
3491
3468
  case 'inside': {
3492
3469
  // When below fit zoom, constrain the camera so that the bounds stay completely within the viewport
3493
- if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w)
3470
+ if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w - bounds.x)
3494
3471
  // When above fit zoom, keep the bounds within padding distance of the viewport edge
3495
3472
  else x = clamp(x, minX + freeW, minX)
3496
3473
  break
3497
3474
  }
3498
3475
  case 'outside': {
3499
3476
  // Constrain the camera so that the bounds never leaves the viewport
3500
- x = clamp(x, px / z - bounds.w, (vsb.w - px) / z)
3477
+ x = clamp(x, px / z - bounds.w - bounds.x, (vsb.w - px) / z - bounds.x)
3501
3478
  break
3502
3479
  }
3503
3480
  case 'free': {
@@ -3522,12 +3499,12 @@ export class Editor extends EventEmitter<TLEventMap> {
3522
3499
  break
3523
3500
  }
3524
3501
  case 'inside': {
3525
- if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h)
3502
+ if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h - bounds.y)
3526
3503
  else y = clamp(y, minY + freeH, minY)
3527
3504
  break
3528
3505
  }
3529
3506
  case 'outside': {
3530
- y = clamp(y, py / z - bounds.h, (vsb.h - py) / z)
3507
+ y = clamp(y, py / z - bounds.h - bounds.y, (vsb.h - py) / z - bounds.y)
3531
3508
  break
3532
3509
  }
3533
3510
  case 'free': {
@@ -3982,9 +3959,9 @@ export class Editor extends EventEmitter<TLEventMap> {
3982
3959
  const remaining = duration - elapsed
3983
3960
  const t = easing(1 - remaining / duration)
3984
3961
 
3985
- const left = start.minX + (end.minX - start.minX) * t
3986
- const top = start.minY + (end.minY - start.minY) * t
3987
- const right = start.maxX + (end.maxX - start.maxX) * t
3962
+ const left = lerp(start.minX, end.minX, t)
3963
+ const top = lerp(start.minY, end.minY, t)
3964
+ const right = lerp(start.maxX, end.maxX, t)
3988
3965
 
3989
3966
  this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), {
3990
3967
  force: true,
@@ -4099,9 +4076,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4099
4076
  let newCy = cy + dy
4100
4077
  let newCz = cz
4101
4078
 
4102
- // animate zoom if z direction is passed in
4079
+ // animate zoom if z direction is passed in. Use an exponential factor so that a single
4080
+ // long frame can't drive the zoom to zero or negative (which would give NaN coordinates)
4103
4081
  if (dirZ !== 0) {
4104
- newCz = cz * (1 + dirZ * currentSpeed * elapsed)
4082
+ newCz = cz * Math.exp(dirZ * currentSpeed * elapsed)
4105
4083
  // Adjust x/y to keep the viewport center fixed while zooming
4106
4084
  const center = this.getViewportScreenCenter()
4107
4085
  newCx += center.x / newCz - center.x / cz
@@ -4686,7 +4664,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4686
4664
  let nextIndex = this.options.maxShapesPerPage * 2
4687
4665
  let nextBackgroundIndex = this.options.maxShapesPerPage
4688
4666
 
4689
- const erasingShapeIds = this.getErasingShapeIds()
4667
+ const erasingShapeIds = new Set(this.getErasingShapeIds())
4690
4668
 
4691
4669
  const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
4692
4670
  const shape = this.getShape(id)
@@ -4694,7 +4672,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4694
4672
 
4695
4673
  if (this.isShapeHidden(shape)) {
4696
4674
  // process children just in case they are overriding the hidden state
4697
- const isErasing = isAncestorErasing || erasingShapeIds.includes(id)
4675
+ const isErasing = isAncestorErasing || erasingShapeIds.has(id)
4698
4676
  for (const childId of this.getSortedChildIdsForParent(id)) {
4699
4677
  addShapeById(childId, opacity, isErasing)
4700
4678
  }
@@ -4706,7 +4684,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4706
4684
  const util = this.getShapeUtil(shape)
4707
4685
 
4708
4686
  if (useEditorState) {
4709
- isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id)
4687
+ isShapeErasing = !isAncestorErasing && erasingShapeIds.has(id)
4710
4688
  if (isShapeErasing) {
4711
4689
  opacity *= 0.32
4712
4690
  }
@@ -5465,11 +5443,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5465
5443
  /** @internal */
5466
5444
  @computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
5467
5445
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
5468
- const pageTransform = this.getShapePageTransform(shape)
5469
- if (!pageTransform) return undefined
5470
-
5471
5446
  return Box.FromPoints(
5472
- pageTransform.applyToPoints(this.getShapeGeometry(shape).boundsVertices)
5447
+ this.getShapePageTransform(shape).applyToPoints(this.getShapeGeometry(shape).boundsVertices)
5473
5448
  )
5474
5449
  })
5475
5450
  }
@@ -5608,7 +5583,13 @@ export class Editor extends EventEmitter<TLEventMap> {
5608
5583
  if (pageMask) {
5609
5584
  if (pageMask.length === 0) return undefined
5610
5585
  const { corners } = pageBounds
5611
- if (corners.every((p, i) => p && Vec.Equals(p, pageMask[i]))) return pageBounds.clone()
5586
+ // the mask may have fewer than four points (e.g. a triangular clip path)
5587
+ if (
5588
+ pageMask.length === corners.length &&
5589
+ corners.every((p, i) => Vec.Equals(p, pageMask[i]))
5590
+ ) {
5591
+ return pageBounds.clone()
5592
+ }
5612
5593
  const intersection = intersectPolygonPolygon(pageMask, corners)
5613
5594
  if (!intersection) return
5614
5595
  return Box.FromPoints(intersection)
@@ -5862,6 +5843,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5862
5843
  const margin = this.options.hitTestMargin / this.getZoomLevel()
5863
5844
  const sortedShapes = this.getCurrentPageShapesSorted()
5864
5845
 
5846
+ // iterate from the top (highest z-index) to find the top-most matching shape
5865
5847
  for (let i = sortedShapes.length - 1; i >= 0; i--) {
5866
5848
  const shape = sortedShapes[i]
5867
5849
  if (shape.type === 'group') continue
@@ -5911,44 +5893,43 @@ export class Editor extends EventEmitter<TLEventMap> {
5911
5893
  const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin())
5912
5894
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5913
5895
 
5914
- const shapesToCheck = (
5915
- opts.renderingOnly
5916
- ? this.getCurrentPageRenderingShapesSorted()
5917
- : this.getCurrentPageShapesSorted()
5918
- ).filter((shape) => {
5919
- // Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
5920
- if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) return false
5896
+ const shapesToCheck = opts.renderingOnly
5897
+ ? this.getCurrentPageRenderingShapesSorted()
5898
+ : this.getCurrentPageShapesSorted()
5921
5899
 
5900
+ for (let i = shapesToCheck.length - 1; i >= 0; i--) {
5901
+ const shape = shapesToCheck[i]
5902
+ // Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
5903
+ if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
5922
5904
  if (
5923
5905
  (shape.isLocked && !hitLocked) ||
5924
5906
  this.isShapeHidden(shape) ||
5925
5907
  this.isShapeOfType(shape, 'group')
5926
- )
5927
- return false
5908
+ ) {
5909
+ continue
5910
+ }
5928
5911
  const pageMask = this.getShapeMask(shape)
5929
- if (pageMask && !pointInPolygon(point, pageMask)) return false
5930
- if (filter && !filter(shape)) return false
5931
- return true
5932
- })
5912
+ if (pageMask && !pointInPolygon(point, pageMask)) continue
5913
+ if (filter && !filter(shape)) continue
5933
5914
 
5934
- for (let i = shapesToCheck.length - 1; i >= 0; i--) {
5935
- const shape = shapesToCheck[i]
5936
5915
  const geometry = this.getShapeGeometry(shape)
5937
5916
  const isGroup = geometry instanceof Group2d
5938
5917
 
5939
5918
  const pointInShapeSpace = this.getPointInShapeSpace(shape, point)
5940
5919
 
5941
- // Check labels first
5920
+ // Check labels first. Only group geometries can carry a label child; a frame-like
5921
+ // shape util may still return a plain geometry.
5942
5922
  const shapeUtil = this.getShapeUtil(shape)
5943
5923
  const isShapeFrameLike = this.isShapeFrameLike(shape)
5944
5924
  if (
5945
- isShapeFrameLike ||
5946
- ((this.isShapeOfType(shape, 'note') ||
5947
- this.isShapeOfType(shape, 'arrow') ||
5948
- (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5949
- shapeUtil.getText(shape)?.trim())
5925
+ isGroup &&
5926
+ (isShapeFrameLike ||
5927
+ ((this.isShapeOfType(shape, 'note') ||
5928
+ this.isShapeOfType(shape, 'arrow') ||
5929
+ (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5930
+ shapeUtil.getText(shape)?.trim()))
5950
5931
  ) {
5951
- for (const childGeometry of (geometry as Group2d).children) {
5932
+ for (const childGeometry of geometry.children) {
5952
5933
  if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) {
5953
5934
  return shape
5954
5935
  }
@@ -6114,15 +6095,18 @@ export class Editor extends EventEmitter<TLEventMap> {
6114
6095
  const margin = opts.margin ?? 0
6115
6096
  const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
6116
6097
 
6117
- // Get all page shapes in z-index order and filter to candidates that pass isPointInShape
6118
- // Frame-like shapes are always checked because their labels can be outside their bounds
6119
- return this.getCurrentPageShapesSorted()
6120
- .filter((shape) => {
6121
- if (this.isShapeHidden(shape)) return false
6122
- if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) return false
6123
- return this.isPointInShape(shape, point, opts)
6124
- })
6125
- .reverse()
6098
+ // Get all page shapes in z-index order and filter to candidates that pass isPointInShape.
6099
+ // Frame-like shapes are always checked because their labels can be outside their bounds.
6100
+ // Iterate backwards so the result is pre-sorted in reverse z-index order (top-most first).
6101
+ const sorted = this.getCurrentPageShapesSorted()
6102
+ const result: TLShape[] = []
6103
+ for (let i = sorted.length - 1; i >= 0; i--) {
6104
+ const shape = sorted[i]
6105
+ if (this.isShapeHidden(shape)) continue
6106
+ if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
6107
+ if (this.isPointInShape(shape, point, opts)) result.push(shape)
6108
+ }
6109
+ return result
6126
6110
  }
6127
6111
 
6128
6112
  /**
@@ -6222,9 +6206,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6222
6206
  if (!freshShape) return new Vec(0, 0)
6223
6207
  if (isPageId(freshShape.parentId)) return Vec.From(point)
6224
6208
 
6225
- const parentTransform = this.getShapePageTransform(freshShape.parentId)
6226
- if (!parentTransform) return Vec.From(point)
6227
- return parentTransform.clone().invert().applyToPoint(point)
6209
+ return this.getShapePageTransform(freshShape.parentId).clone().invert().applyToPoint(point)
6228
6210
  }
6229
6211
 
6230
6212
  /**
@@ -6391,26 +6373,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6391
6373
  * @public
6392
6374
  */
6393
6375
  isShapeInPage(shape: TLShape | TLShapeId, pageId = this.getCurrentPageId()): boolean {
6394
- const id = typeof shape === 'string' ? shape : shape.id
6395
- const shapeToCheck = this.getShape(id)
6396
- if (!shapeToCheck) return false
6397
-
6398
- let shapeIsInPage = false
6399
-
6400
- if (shapeToCheck.parentId === pageId) {
6401
- shapeIsInPage = true
6402
- } else {
6403
- let parent = this.getShape(shapeToCheck.parentId)
6404
- isInPageSearch: while (parent) {
6405
- if (parent.parentId === pageId) {
6406
- shapeIsInPage = true
6407
- break isInPageSearch
6408
- }
6409
- parent = this.getShape(parent.parentId)
6410
- }
6411
- }
6412
-
6413
- return shapeIsInPage
6376
+ return this.getAncestorPageId(shape) === pageId
6414
6377
  }
6415
6378
 
6416
6379
  /**
@@ -6521,13 +6484,8 @@ export class Editor extends EventEmitter<TLEventMap> {
6521
6484
  for (let i = 0; i < shapesToReparent.length; i++) {
6522
6485
  const shape = shapesToReparent[i]
6523
6486
 
6524
- const pageTransform = this.getShapePageTransform(shape)!
6525
- if (!pageTransform) continue
6526
-
6527
- const pagePoint = pageTransform.point()
6528
- if (!pagePoint) continue
6529
-
6530
- const newPoint = invertedParentTransform.applyToPoint(pagePoint)
6487
+ const pageTransform = this.getShapePageTransform(shape)
6488
+ const newPoint = invertedParentTransform.applyToPoint(pageTransform.point())
6531
6489
  const newRotation = pageTransform.rotation() - parentPageRotation
6532
6490
 
6533
6491
  if (shape.id === parentId) {
@@ -6628,7 +6586,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6628
6586
  */
6629
6587
  getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId> {
6630
6588
  const shapeIds = new Set<TLShapeId>()
6631
- for (const shape of ids.map((id) => this.getShape(id)!).sort(sortByIndex)) {
6589
+ for (const shape of compact(ids.map((id) => this.getShape(id))).sort(sortByIndex)) {
6632
6590
  shapeIds.add(shape.id)
6633
6591
  this.visitDescendants(shape, (descendantId) => {
6634
6592
  shapeIds.add(descendantId)
@@ -6652,6 +6610,9 @@ export class Editor extends EventEmitter<TLEventMap> {
6652
6610
  const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
6653
6611
  (s) => !s.isLocked && !this.isShapeHidden(s)
6654
6612
  )
6613
+ // Descendants of the dragged shapes can't be the target, otherwise dragging a frame out of
6614
+ // its parent reports a nested child as the target and the parent never sees the drag leave
6615
+ const excludedIds = this.getShapeAndDescendantIds(draggingShapes.map((s) => s.id))
6655
6616
 
6656
6617
  const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
6657
6618
  hitInside: true,
@@ -6661,7 +6622,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6661
6622
  !droppingShapes.includes(s) &&
6662
6623
  !s.isLocked &&
6663
6624
  !this.isShapeHidden(s) &&
6664
- !draggingShapes.includes(s)
6625
+ !excludedIds.has(s.id)
6665
6626
  )
6666
6627
 
6667
6628
  for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
@@ -7035,8 +6996,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7035
6996
  for (const id of ids) {
7036
6997
  const shape = this.getShape(id)!
7037
6998
  const localDelta = Vec.From(offset)
7038
- const parentTransform = this.getShapeParentTransform(shape)
7039
- if (parentTransform) localDelta.rot(-parentTransform.rotation())
6999
+ localDelta.rot(-this.getShapeParentTransform(shape).rotation())
7040
7000
 
7041
7001
  changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)))
7042
7002
  }
@@ -7106,7 +7066,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7106
7066
  let ox = 0
7107
7067
  let oy = 0
7108
7068
 
7109
- if (offset && initialIds.has(originalId)) {
7069
+ // Only offset the roots of the duplicated tree: a descendant that was also passed in
7070
+ // follows its duplicated parent, so offsetting it too would move it twice
7071
+ if (
7072
+ offset &&
7073
+ initialIds.has(originalId) &&
7074
+ !shapeIdSet.has(originalShape.parentId as TLShapeId)
7075
+ ) {
7110
7076
  const parentTransform = this.getShapeParentTransform(originalShape)
7111
7077
  const vec = new Vec(offset.x, offset.y).rot(-parentTransform!.rotation())
7112
7078
  ox = vec.x
@@ -7272,27 +7238,17 @@ export class Editor extends EventEmitter<TLEventMap> {
7272
7238
 
7273
7239
  if (this.getIsReadonly() || ids.length === 0) return this
7274
7240
 
7275
- let allLocked = true,
7276
- allUnlocked = true
7241
+ let allLocked = true
7277
7242
  const shapesToToggle: TLShape[] = []
7278
7243
  for (const id of ids) {
7279
7244
  const shape = this.getShape(id)
7280
7245
  if (shape) {
7281
7246
  shapesToToggle.push(shape)
7282
- if (shape.isLocked) {
7283
- allUnlocked = false
7284
- } else {
7285
- allLocked = false
7286
- }
7247
+ if (!shape.isLocked) allLocked = false
7287
7248
  }
7288
7249
  }
7289
7250
  this.run(() => {
7290
- if (allUnlocked) {
7291
- this.updateShapes(
7292
- shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
7293
- )
7294
- this.setSelectedShapes([])
7295
- } else if (allLocked) {
7251
+ if (allLocked) {
7296
7252
  this.updateShapes(
7297
7253
  shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
7298
7254
  )
@@ -7300,6 +7256,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7300
7256
  this.updateShapes(
7301
7257
  shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
7302
7258
  )
7259
+ this.setSelectedShapes([])
7303
7260
  }
7304
7261
  })
7305
7262
 
@@ -7439,9 +7396,12 @@ export class Editor extends EventEmitter<TLEventMap> {
7439
7396
 
7440
7397
  // optionally filter to axis-aligned shapes (rotation is a multiple of 90 degrees)
7441
7398
  if (opts?.filterAxisAligned) {
7442
- freshShapes = freshShapes.filter(
7443
- (s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
7444
- )
7399
+ freshShapes = freshShapes.filter((s) => {
7400
+ // Page rotations come back through atan2 on a composed matrix, so a 270° shape can land
7401
+ // a hair either side of the multiple; an exact === 0 check drops it
7402
+ const remainder = Math.abs(this.getShapePageTransform(s).rotation() % HALF_PI)
7403
+ return Math.min(remainder, HALF_PI - remainder) < 1e-9
7404
+ })
7445
7405
  }
7446
7406
 
7447
7407
  const clusters: { shapes: TLShape[]; pageBounds: Box }[] = []
@@ -7662,13 +7622,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7662
7622
 
7663
7623
  let shapeGap: number = 0
7664
7624
 
7625
+ // Stack in spatial order rather than input (z) order, otherwise shapes swap places
7626
+ shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
7627
+
7665
7628
  if (_gap === 0) {
7666
7629
  // note: this is not used in the current tldraw.com; there we use a specified stack
7667
7630
 
7668
7631
  const gaps: Record<number, number> = {}
7669
7632
 
7670
- shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
7671
-
7672
7633
  // Collect all of the gaps between shapes. We want to find
7673
7634
  // patterns (equal gaps between shapes) and use the most common
7674
7635
  // one as the gap for all of the shapes.
@@ -7721,8 +7682,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7721
7682
  // todo: ensure that the parent isn't being aligned together with its children
7722
7683
  const parent = this.getShapeParent(shape)
7723
7684
  if (parent) {
7724
- const parentTransform = this.getShapePageTransform(parent)
7725
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7685
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
7726
7686
  }
7727
7687
 
7728
7688
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -7772,10 +7732,11 @@ export class Editor extends EventEmitter<TLEventMap> {
7772
7732
 
7773
7733
  const maxWidth = commonBounds.width
7774
7734
 
7775
- // sort the shape clusters by width and then height, descending
7735
+ // sort the shape clusters by width and then height, descending: potpack fills each row's
7736
+ // right-hand space with the shapes that follow, so they must be no taller than the row
7776
7737
  shapeClustersToPack
7777
- .sort((a, b) => a.pageBounds.width - b.pageBounds.width)
7778
- .sort((a, b) => a.pageBounds.height - b.pageBounds.height)
7738
+ .sort((a, b) => b.pageBounds.width - a.pageBounds.width)
7739
+ .sort((a, b) => b.pageBounds.height - a.pageBounds.height)
7779
7740
 
7780
7741
  // Start with is (sort of) the square of the area
7781
7742
  const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
@@ -7845,8 +7806,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7845
7806
 
7846
7807
  const parent = this.getShapeParent(shape)
7847
7808
  if (parent) {
7848
- const parentTransform = this.getShapeParentTransform(shape)
7849
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7809
+ shapeDelta.rot(-this.getShapeParentTransform(shape).rotation())
7850
7810
  }
7851
7811
 
7852
7812
  shapeDelta.add(shape)
@@ -7936,8 +7896,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7936
7896
  // todo: ensure that the parent isn't being aligned together with its children
7937
7897
  const parent = this.getShapeParent(shape)
7938
7898
  if (parent) {
7939
- const parentTransform = this.getShapePageTransform(parent)
7940
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7899
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
7941
7900
  }
7942
7901
 
7943
7902
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -8037,8 +7996,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8037
7996
  // todo: ensure that the parent isn't being aligned together with its children
8038
7997
  const parent = this.getShapeParent(shape)
8039
7998
  if (parent) {
8040
- const parentTransform = this.getShapePageTransform(parent)
8041
- if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
7999
+ shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
8042
8000
  }
8043
8001
 
8044
8002
  shapeDelta.add(shape) // add the shape's x and y to the delta
@@ -8106,8 +8064,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8106
8064
  for (const shape of shapes) {
8107
8065
  // First translate
8108
8066
  const shapeLocalOffset = localOffset.clone()
8109
- const parentTransform = this.getShapeParentTransform(shape)
8110
- if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
8067
+ shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
8111
8068
  shapeLocalOffset.add(shape)
8112
8069
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
8113
8070
  this.updateShape(changes)
@@ -8178,8 +8135,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8178
8135
  for (const shape of shapes) {
8179
8136
  // First translate
8180
8137
  const shapeLocalOffset = localOffset.clone()
8181
- const parentTransform = this.getShapeParentTransform(shape)
8182
- if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
8138
+ shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
8183
8139
  shapeLocalOffset.add(shape)
8184
8140
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
8185
8141
  this.updateShape(changes)
@@ -8243,12 +8199,9 @@ export class Editor extends EventEmitter<TLEventMap> {
8243
8199
  const pageTransform = opts.initialPageTransform
8244
8200
  ? Mat.Cast(opts.initialPageTransform)
8245
8201
  : this.getShapePageTransform(id)
8246
- if (!pageTransform) return null
8247
8202
 
8248
8203
  const pageRotation = pageTransform.rotation()
8249
8204
 
8250
- if (pageRotation == null) return null
8251
-
8252
8205
  const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
8253
8206
 
8254
8207
  const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
@@ -8485,7 +8438,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8485
8438
  )
8486
8439
 
8487
8440
  // now calculate how far away the shape is from where it needs to be
8488
- const pageTransform = this.getShapePageTransform(id)!
8441
+ const pageTransform = this.getShapePageTransform(id)
8489
8442
  // We need to use the local bounds center transformed to page space, not the axis-aligned
8490
8443
  // page bounds center. This is because the page bounds are axis-aligned and their center
8491
8444
  // changes when the rotation changes, but we want to use the same reference point as
@@ -8493,7 +8446,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8493
8446
  const currentLocalBounds = this.getShapeGeometry(id).bounds
8494
8447
  const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
8495
8448
  const shapePageTransformOrigin = pageTransform.point()
8496
- if (!currentPageCenter || !shapePageTransformOrigin) return this
8497
8449
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
8498
8450
 
8499
8451
  // and finally figure out what the shape's new position should be
@@ -8881,10 +8833,10 @@ export class Editor extends EventEmitter<TLEventMap> {
8881
8833
 
8882
8834
  updates.push({
8883
8835
  ...end,
8884
- x: start.x + (end.x - start.x) * t,
8885
- y: start.y + (end.y - start.y) * t,
8886
- opacity: start.opacity + (end.opacity - start.opacity) * t,
8887
- rotation: start.rotation + (end.rotation - start.rotation) * t,
8836
+ x: lerp(start.x, end.x, t),
8837
+ y: lerp(start.y, end.y, t),
8838
+ opacity: lerp(start.opacity, end.opacity, t),
8839
+ rotation: lerp(start.rotation, end.rotation, t),
8888
8840
  props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props,
8889
8841
  })
8890
8842
  }
@@ -9100,7 +9052,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9100
9052
  * @public
9101
9053
  */
9102
9054
  updateShapes<T extends TLShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
9103
- const compactedPartials: TLShapePartial<T>[] = Array(partials.length)
9055
+ const compactedPartials: TLShapePartial<T>[] = []
9104
9056
 
9105
9057
  for (let i = 0, n = partials.length; i < n; i++) {
9106
9058
  const partial = partials[i]
@@ -9246,26 +9198,25 @@ export class Editor extends EventEmitter<TLEventMap> {
9246
9198
  /* --------------------- Styles --------------------- */
9247
9199
 
9248
9200
  /**
9249
- * Get all the current styles among the users selected shapes
9201
+ * Groups have no styles of their own: a style read or write on a selection applies to the
9202
+ * non-group shapes beneath each group, however deeply nested. Returns those shapes.
9250
9203
  *
9251
9204
  * @internal
9252
9205
  */
9253
- private _extractSharedStyles(shape: TLShape, sharedStyleMap: SharedStyleMap) {
9254
- if (this.isShapeOfType(shape, 'group')) {
9255
- // For groups, ignore the styles of the group shape and instead include the styles of the
9256
- // group's children. These are the shapes that would have their styles changed if the
9257
- // user called `setStyle` on the current selection.
9258
- const childIds = this._parentIdsToChildIds.get()[shape.id]
9259
- if (!childIds) return
9260
-
9261
- for (let i = 0, n = childIds.length; i < n; i++) {
9262
- this._extractSharedStyles(this.getShape(childIds[i])!, sharedStyleMap)
9263
- }
9264
- } else {
9265
- for (const [style, propKey] of this.styleProps[shape.type]) {
9266
- sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey))
9206
+ private _getStyleableShapes(shapes: TLShape[]): TLShape[] {
9207
+ const result: TLShape[] = []
9208
+ const visit = (shape: TLShape) => {
9209
+ if (this.isShapeOfType(shape, 'group')) {
9210
+ for (const childId of this.getSortedChildIdsForParent(shape.id)) {
9211
+ const child = this.getShape(childId)
9212
+ if (child) visit(child)
9213
+ }
9214
+ } else {
9215
+ result.push(shape)
9267
9216
  }
9268
9217
  }
9218
+ for (const shape of shapes) visit(shape)
9219
+ return result
9269
9220
  }
9270
9221
 
9271
9222
  /**
@@ -9275,11 +9226,11 @@ export class Editor extends EventEmitter<TLEventMap> {
9275
9226
  */
9276
9227
  @computed
9277
9228
  private _getSelectionSharedStyles(): ReadonlySharedStyleMap {
9278
- const selectedShapes = this.getSelectedShapes()
9279
-
9280
9229
  const sharedStyles = new SharedStyleMap()
9281
- for (const selectedShape of selectedShapes) {
9282
- this._extractSharedStyles(selectedShape, sharedStyles)
9230
+ for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
9231
+ for (const [style, propKey] of this.styleProps[shape.type]) {
9232
+ sharedStyles.applyValue(style, getOwnProperty(shape.props, propKey))
9233
+ }
9283
9234
  }
9284
9235
 
9285
9236
  return sharedStyles
@@ -9337,18 +9288,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9337
9288
  if (!currentTool) return styles
9338
9289
 
9339
9290
  if (currentTool.shapeType) {
9340
- if (
9341
- currentTool.shapeType === 'frame' &&
9342
- !(this.getShapeUtil('frame')!.options as any).showColors
9343
- ) {
9344
- for (const style of this.styleProps[currentTool.shapeType].keys()) {
9345
- if (style.id === 'tldraw:color') continue
9346
- styles.applyValue(style, this.getStyleForNextShape(style))
9347
- }
9348
- } else {
9349
- for (const style of this.styleProps[currentTool.shapeType].keys()) {
9350
- styles.applyValue(style, this.getStyleForNextShape(style))
9351
- }
9291
+ for (const style of this.styleProps[currentTool.shapeType].keys()) {
9292
+ styles.applyValue(style, this.getStyleForNextShape(style))
9352
9293
  }
9353
9294
  }
9354
9295
 
@@ -9364,27 +9305,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9364
9305
  */
9365
9306
  @computed getSharedOpacity(): SharedStyle<number> {
9366
9307
  if (this.isIn('select') && this.getSelectedShapeIds().length > 0) {
9367
- const shapesToCheck: TLShape[] = []
9368
- const addShape = (shapeId: TLShapeId) => {
9369
- const shape = this.getShape(shapeId)
9370
- if (!shape) return
9371
- // For groups, ignore the opacity of the group shape and instead include
9372
- // the opacity of the group's children. These are the shapes that would have
9373
- // their opacity changed if the user called `setOpacity` on the current selection.
9374
- if (this.isShapeOfType(shape, 'group')) {
9375
- for (const childId of this.getSortedChildIdsForParent(shape.id)) {
9376
- addShape(childId)
9377
- }
9378
- } else {
9379
- shapesToCheck.push(shape)
9380
- }
9381
- }
9382
- for (const shapeId of this.getSelectedShapeIds()) {
9383
- addShape(shapeId)
9384
- }
9385
-
9386
9308
  let opacity: number | null = null
9387
- for (const shape of shapesToCheck) {
9309
+ for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
9388
9310
  if (opacity === null) {
9389
9311
  opacity = shape.opacity
9390
9312
  } else if (opacity !== shape.opacity) {
@@ -9427,33 +9349,12 @@ export class Editor extends EventEmitter<TLEventMap> {
9427
9349
  const selectedShapes = this.getSelectedShapes()
9428
9350
 
9429
9351
  if (selectedShapes.length > 0) {
9430
- const shapesToUpdate: TLShape[] = []
9431
-
9432
- // We can have many deep levels of grouped shape
9433
- // Making a recursive function to look through all the levels
9434
- const addShapeById = (shape: TLShape) => {
9435
- if (this.isShapeOfType(shape, 'group')) {
9436
- const childIds = this.getSortedChildIdsForParent(shape)
9437
- for (const childId of childIds) {
9438
- addShapeById(this.getShape(childId)!)
9439
- }
9440
- } else {
9441
- shapesToUpdate.push(shape)
9442
- }
9443
- }
9444
-
9445
- for (const id of selectedShapes) {
9446
- addShapeById(id)
9447
- }
9448
-
9449
9352
  this.updateShapes(
9450
- shapesToUpdate.map((shape) => {
9451
- return {
9452
- id: shape.id,
9453
- type: shape.type,
9454
- opacity,
9455
- }
9456
- })
9353
+ this._getStyleableShapes(selectedShapes).map((shape) => ({
9354
+ id: shape.id,
9355
+ type: shape.type,
9356
+ opacity,
9357
+ }))
9457
9358
  )
9458
9359
  }
9459
9360
 
@@ -9507,43 +9408,19 @@ export class Editor extends EventEmitter<TLEventMap> {
9507
9408
  const selectedShapes = this.getSelectedShapes()
9508
9409
 
9509
9410
  if (selectedShapes.length > 0) {
9510
- const updates: {
9511
- util: ShapeUtil
9512
- originalShape: TLShape
9513
- updatePartial: TLShapePartial
9514
- }[] = []
9515
-
9516
- // We can have many deep levels of grouped shape
9517
- // Making a recursive function to look through all the levels
9518
- const addShapeById = (shape: TLShape) => {
9519
- if (this.isShapeOfType(shape, 'group')) {
9520
- const childIds = this.getSortedChildIdsForParent(shape.id)
9521
- for (const childId of childIds) {
9522
- addShapeById(this.getShape(childId)!)
9523
- }
9524
- } else {
9525
- const util = this.getShapeUtil(shape)
9526
- const stylePropKey = this.styleProps[shape.type].get(style)
9527
- if (stylePropKey) {
9528
- const shapePartial: TLShapePartial = {
9529
- id: shape.id,
9530
- type: shape.type,
9531
- props: { [stylePropKey]: value },
9532
- }
9533
- updates.push({
9534
- util,
9535
- originalShape: shape,
9536
- updatePartial: shapePartial,
9537
- })
9538
- }
9411
+ const updates: TLShapePartial[] = []
9412
+ for (const shape of this._getStyleableShapes(selectedShapes)) {
9413
+ const stylePropKey = this.styleProps[shape.type].get(style)
9414
+ if (stylePropKey) {
9415
+ updates.push({
9416
+ id: shape.id,
9417
+ type: shape.type,
9418
+ props: { [stylePropKey]: value },
9419
+ })
9539
9420
  }
9540
9421
  }
9541
9422
 
9542
- for (const shape of selectedShapes) {
9543
- addShapeById(shape)
9544
- }
9545
-
9546
- this.updateShapes(updates.map(({ updatePartial }) => updatePartial))
9423
+ this.updateShapes(updates)
9547
9424
  }
9548
9425
 
9549
9426
  return this
@@ -10075,7 +9952,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10075
9952
  if (shapeIdMap.has(newShape.parentId)) {
10076
9953
  newShape.parentId = shapeIdMap.get(oldShape.parentId)!
10077
9954
  } else {
10078
- rootShapeIds.push(newShape.id)
10079
9955
  // newShape.parentId = pasteParentId
10080
9956
  newShape.index = index
10081
9957
  index = getIndexAbove(index)
@@ -10118,10 +9994,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10118
9994
  (asset.type === 'video' && asset.props.src?.startsWith('data:video'))
10119
9995
  ) {
10120
9996
  // it's src is a base64 image or video; we need to create a new asset without the src,
10121
- // then create a new asset from the original src. So we save a copy of the original asset,
10122
- // then delete the src from the original asset.
10123
- assetsToUpdate.push(structuredClone(asset as TLImageAsset | TLVideoAsset))
10124
- asset.props.src = null
9997
+ // then create a new asset from the original src. So we keep the original asset for the
9998
+ // upload and create a copy with its src removed. Copy rather than mutate: when no
9999
+ // migration applies, migrateStoreSnapshot hands back the caller's own record objects.
10000
+ assetsToUpdate.push(asset as TLImageAsset | TLVideoAsset)
10001
+ const assetWithoutSrc = structuredClone(asset as TLImageAsset | TLVideoAsset)
10002
+ assetWithoutSrc.props.src = null
10003
+ assetsToCreate.push(assetWithoutSrc)
10004
+ continue
10125
10005
  }
10126
10006
 
10127
10007
  // Add the asset to the list of assets to create
@@ -11116,7 +10996,10 @@ export class Editor extends EventEmitter<TLEventMap> {
11116
10996
  if (info.name === 'cancel' || info.name === 'complete') {
11117
10997
  this.inputs.setIsDragging(false)
11118
10998
 
11119
- if (this.inputs.getIsPanning()) {
10999
+ // A pan owned by a held spacebar outlives the cancelled interaction;
11000
+ // key_up ends it. Otherwise Escape mid-pan stops the camera until
11001
+ // the user releases and re-presses Space (#10446).
11002
+ if (this.inputs.getIsPanning() && !this.inputs.keys.has('Space')) {
11120
11003
  this.inputs.setIsPanning(false)
11121
11004
  this.inputs.setIsSpacebarPanning(false)
11122
11005
  this.setCursor({ type: this._prevCursor, rotation: 0 })
@@ -11870,10 +11753,10 @@ function withIsolatedShapes<T>(
11870
11753
 
11871
11754
  function getCameraFitXFitY(editor: Editor, cameraOptions: TLCameraOptions) {
11872
11755
  if (!cameraOptions.constraints) throw Error('Should have constraints here')
11873
- const {
11874
- padding: { x: px, y: py },
11875
- } = cameraOptions.constraints
11876
11756
  const vsb = editor.getViewportScreenBounds()
11757
+ // Clamp padding to half the viewport size on either dimension, as getConstrainedCamera does
11758
+ const px = Math.min(cameraOptions.constraints.padding.x, vsb.w / 2)
11759
+ const py = Math.min(cameraOptions.constraints.padding.y, vsb.h / 2)
11877
11760
  const bounds = Box.From(cameraOptions.constraints.bounds)
11878
11761
  const zx = (vsb.w - px * 2) / bounds.w
11879
11762
  const zy = (vsb.h - py * 2) / bounds.h