@tldraw/editor 4.3.0-next.f4772c19540d → 4.4.0-canary.29afdff6bb04

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 (206) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +503 -155
  3. package/dist-cjs/index.js +8 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  7. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -5
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/constants.js +1 -3
  11. package/dist-cjs/lib/constants.js.map +2 -2
  12. package/dist-cjs/lib/editor/Editor.js +346 -291
  13. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  14. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  15. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  16. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -23
  17. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  18. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  19. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  23. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  25. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  27. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  30. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +181 -0
  31. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  32. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  33. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/shapes/BaseBoxShapeUtil.js.map +1 -1
  35. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  36. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  37. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  38. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  39. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  40. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.js.map +2 -2
  41. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  42. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  43. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  44. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  45. package/dist-cjs/lib/exports/parseCss.js +1 -1
  46. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  47. package/dist-cjs/lib/globals/environment.js +45 -9
  48. package/dist-cjs/lib/globals/environment.js.map +2 -2
  49. package/dist-cjs/lib/globals/menus.js +1 -1
  50. package/dist-cjs/lib/globals/menus.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  52. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  54. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  55. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  56. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  57. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  58. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  59. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  60. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  61. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  62. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  63. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  64. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  65. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  66. package/dist-cjs/lib/options.js +6 -1
  67. package/dist-cjs/lib/options.js.map +2 -2
  68. package/dist-cjs/lib/primitives/Box.js +3 -0
  69. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  70. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  71. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  72. package/dist-cjs/lib/utils/reparenting.js.map +2 -2
  73. package/dist-cjs/lib/utils/rotation.js +1 -1
  74. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  75. package/dist-cjs/version.js +3 -3
  76. package/dist-cjs/version.js.map +1 -1
  77. package/dist-esm/index.d.mts +503 -155
  78. package/dist-esm/index.mjs +9 -2
  79. package/dist-esm/index.mjs.map +2 -2
  80. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  81. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  82. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  83. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -5
  84. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  85. package/dist-esm/lib/constants.mjs +1 -3
  86. package/dist-esm/lib/constants.mjs.map +2 -2
  87. package/dist-esm/lib/editor/Editor.mjs +347 -294
  88. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  89. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  90. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  91. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -23
  92. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  93. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  94. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  95. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  96. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  97. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  98. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  99. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  100. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  101. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  102. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  103. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  104. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  105. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +161 -0
  106. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  107. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  108. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  109. package/dist-esm/lib/editor/shapes/BaseBoxShapeUtil.mjs.map +1 -1
  110. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  111. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  112. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  113. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  114. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  115. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.mjs.map +2 -2
  116. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  117. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  118. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  119. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  120. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  121. package/dist-esm/lib/globals/environment.mjs +45 -9
  122. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  123. package/dist-esm/lib/globals/menus.mjs +1 -1
  124. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  125. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  126. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  127. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  128. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  129. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  130. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  131. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  132. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  133. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  134. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  135. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  136. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  137. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  138. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  139. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  140. package/dist-esm/lib/options.mjs +6 -1
  141. package/dist-esm/lib/options.mjs.map +2 -2
  142. package/dist-esm/lib/primitives/Box.mjs +3 -0
  143. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  144. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  145. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  146. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  147. package/dist-esm/lib/utils/rotation.mjs +1 -1
  148. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  149. package/dist-esm/version.mjs +3 -3
  150. package/dist-esm/version.mjs.map +1 -1
  151. package/editor.css +14 -12
  152. package/package.json +21 -17
  153. package/src/index.ts +5 -1
  154. package/src/lib/components/ErrorBoundary.tsx +1 -1
  155. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  156. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -8
  157. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  158. package/src/lib/constants.ts +0 -2
  159. package/src/lib/editor/Editor.test.ts +150 -10
  160. package/src/lib/editor/Editor.ts +533 -384
  161. package/src/lib/editor/bindings/BindingUtil.ts +15 -9
  162. package/src/lib/editor/derivations/bindingsIndex.ts +2 -2
  163. package/src/lib/editor/derivations/notVisibleShapes.ts +21 -33
  164. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  165. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  166. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  167. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  168. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  169. package/src/lib/editor/managers/FontManager/FontManager.test.ts +14 -4
  170. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  171. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  172. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  173. package/src/lib/editor/managers/SnapManager/SnapManager.ts +4 -4
  174. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  175. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +215 -0
  176. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  177. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  178. package/src/lib/editor/shapes/BaseBoxShapeUtil.tsx +2 -2
  179. package/src/lib/editor/shapes/ShapeUtil.ts +72 -32
  180. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  181. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -3
  182. package/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts +2 -1
  183. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +6 -6
  184. package/src/lib/editor/types/emit-types.ts +3 -1
  185. package/src/lib/exports/getSvgJsx.test.ts +10 -19
  186. package/src/lib/exports/getSvgJsx.tsx +2 -5
  187. package/src/lib/exports/parseCss.test.ts +1 -0
  188. package/src/lib/exports/parseCss.ts +1 -1
  189. package/src/lib/globals/environment.ts +65 -10
  190. package/src/lib/globals/menus.ts +1 -1
  191. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  192. package/src/lib/hooks/useEvent.tsx +1 -1
  193. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  194. package/src/lib/hooks/useGestureEvents.ts +2 -2
  195. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  196. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  197. package/src/lib/hooks/useScreenBounds.ts +1 -1
  198. package/src/lib/hooks/useStateAttribute.ts +4 -1
  199. package/src/lib/hooks/useTransform.ts +1 -1
  200. package/src/lib/hooks/useZoomCss.ts +3 -8
  201. package/src/lib/options.ts +32 -0
  202. package/src/lib/primitives/Box.ts +9 -0
  203. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  204. package/src/lib/utils/reparenting.ts +5 -5
  205. package/src/lib/utils/rotation.ts +1 -1
  206. package/src/version.ts +3 -3
@@ -21,7 +21,6 @@ import {
21
21
  PageRecordType,
22
22
  StyleProp,
23
23
  StylePropValue,
24
- TLArrowShape,
25
24
  TLAsset,
26
25
  TLAssetId,
27
26
  TLAssetPartial,
@@ -30,12 +29,12 @@ import {
30
29
  TLBindingId,
31
30
  TLBindingUpdate,
32
31
  TLCamera,
32
+ TLCreateShapePartial,
33
33
  TLCursor,
34
34
  TLCursorType,
35
35
  TLDOCUMENT_ID,
36
36
  TLDocument,
37
37
  TLFrameShape,
38
- TLGeoShape,
39
38
  TLGroupShape,
40
39
  TLHandle,
41
40
  TLINSTANCE_ID,
@@ -43,8 +42,6 @@ import {
43
42
  TLInstance,
44
43
  TLInstancePageState,
45
44
  TLInstancePresence,
46
- TLNoteShape,
47
- TLPOINTER_ID,
48
45
  TLPage,
49
46
  TLPageId,
50
47
  TLParentId,
@@ -54,8 +51,6 @@ import {
54
51
  TLShapePartial,
55
52
  TLStore,
56
53
  TLStoreSnapshot,
57
- TLUnknownBinding,
58
- TLUnknownShape,
59
54
  TLVideoAsset,
60
55
  createBindingId,
61
56
  createShapeId,
@@ -113,7 +108,6 @@ import {
113
108
  MIDDLE_MOUSE_BUTTON,
114
109
  RIGHT_MOUSE_BUTTON,
115
110
  STYLUS_ERASER_BUTTON,
116
- ZOOM_TO_FIT_PADDING,
117
111
  } from '../constants'
118
112
  import { exportToSvg } from '../exports/exportToSvg'
119
113
  import { getSvgAsImage } from '../exports/getSvgAsImage'
@@ -139,7 +133,6 @@ import {
139
133
  parseDeepLinkString,
140
134
  } from '../utils/deepLinks'
141
135
  import { getIncrementedName } from '../utils/getIncrementedName'
142
- import { isAccelKey } from '../utils/keyboard'
143
136
  import { getReorderingShapesChanges } from '../utils/reorderShapes'
144
137
  import { TLTextOptions, TiptapEditor } from '../utils/richText'
145
138
  import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
@@ -153,22 +146,19 @@ import { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManage
153
146
  import { FocusManager } from './managers/FocusManager/FocusManager'
154
147
  import { FontManager } from './managers/FontManager/FontManager'
155
148
  import { HistoryManager } from './managers/HistoryManager/HistoryManager'
149
+ import { InputsManager } from './managers/InputsManager/InputsManager'
156
150
  import { ScribbleManager } from './managers/ScribbleManager/ScribbleManager'
157
151
  import { SnapManager } from './managers/SnapManager/SnapManager'
152
+ import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndexManager'
158
153
  import { TextManager } from './managers/TextManager/TextManager'
159
154
  import { TickManager } from './managers/TickManager/TickManager'
160
155
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
161
- import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
156
+ import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
162
157
  import { RootState } from './tools/RootState'
163
158
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
164
159
  import { TLContent } from './types/clipboard-types'
165
160
  import { TLEventMap } from './types/emit-types'
166
- import {
167
- TLEventInfo,
168
- TLPinchEventInfo,
169
- TLPointerEventInfo,
170
- TLWheelEventInfo,
171
- } from './types/event-types'
161
+ import { TLEventInfo, TLPointerEventInfo } from './types/event-types'
172
162
  import { TLExternalAsset, TLExternalContent } from './types/external-content'
173
163
  import { TLHistoryBatchOptions } from './types/history-types'
174
164
  import {
@@ -199,7 +189,7 @@ export type TLResizeShapeOptions = Partial<{
199
189
  /** @public */
200
190
  export interface TLEditorOptions {
201
191
  /**
202
- * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading
192
+ * The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading
203
193
  * from a server or database.
204
194
  */
205
195
  store: TLStore
@@ -319,6 +309,9 @@ export class Editor extends EventEmitter<TLEventMap> {
319
309
 
320
310
  this.snaps = new SnapManager(this)
321
311
 
312
+ this._spatialIndex = new SpatialIndexManager(this)
313
+ this.disposables.add(() => this._spatialIndex.dispose())
314
+
322
315
  this.disposables.add(this.timers.dispose)
323
316
 
324
317
  this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
@@ -337,6 +330,8 @@ export class Editor extends EventEmitter<TLEventMap> {
337
330
 
338
331
  this._tickManager = new TickManager(this)
339
332
 
333
+ this.inputs = new InputsManager(this)
334
+
340
335
  class NewRoot extends RootState {
341
336
  static override initial = initialState ?? ''
342
337
  }
@@ -447,7 +442,7 @@ export class Editor extends EventEmitter<TLEventMap> {
447
442
  let deletedBindings = new Map<TLBindingId, BindingOnDeleteOptions<any>>()
448
443
  const deletedShapeIds = new Set<TLShapeId>()
449
444
  const invalidParents = new Set<TLShapeId>()
450
- let invalidBindingTypes = new Set<string>()
445
+ let invalidBindingTypes = new Set<TLBinding['type']>()
451
446
  this.disposables.add(
452
447
  this.sideEffects.registerOperationCompleteHandler(() => {
453
448
  // this needs to be cleared here because further effects may delete more shapes
@@ -710,7 +705,7 @@ export class Editor extends EventEmitter<TLEventMap> {
710
705
  if (filtered.length > 0) {
711
706
  const commonGroupAncestor = this.findCommonAncestor(
712
707
  compact(filtered.map((id) => this.getShape(id))),
713
- (shape) => this.isShapeOfType<TLGroupShape>(shape, 'group')
708
+ (shape) => this.isShapeOfType(shape, 'group')
714
709
  )
715
710
 
716
711
  if (commonGroupAncestor) {
@@ -871,7 +866,7 @@ export class Editor extends EventEmitter<TLEventMap> {
871
866
  }
872
867
 
873
868
  /**
874
- * A set of functions to call when the app is disposed.
869
+ * A set of functions to call when the editor is disposed.
875
870
  *
876
871
  * @public
877
872
  */
@@ -884,16 +879,28 @@ export class Editor extends EventEmitter<TLEventMap> {
884
879
  */
885
880
  isDisposed = false
886
881
 
887
- /** @internal */
888
- private readonly _tickManager
882
+ /**
883
+ * A manager for the editor's tick events.
884
+ *
885
+ * @internal */
886
+ private readonly _tickManager: TickManager
889
887
 
890
888
  /**
891
- * A manager for the app's snapping feature.
889
+ * A manager for the editor's input state.
890
+ *
891
+ * @public
892
+ */
893
+ readonly inputs: InputsManager
894
+
895
+ /**
896
+ * A manager for the editor's snapping feature.
892
897
  *
893
898
  * @public
894
899
  */
895
900
  readonly snaps: SnapManager
896
901
 
902
+ private readonly _spatialIndex: SpatialIndexManager
903
+
897
904
  /**
898
905
  * A manager for the any asynchronous events and making sure they're
899
906
  * cleaned up upon disposal.
@@ -973,6 +980,7 @@ export class Editor extends EventEmitter<TLEventMap> {
973
980
  this.disposables.clear()
974
981
  this.store.dispose()
975
982
  this.isDisposed = true
983
+ this.emit('dispose')
976
984
  }
977
985
 
978
986
  /* ------------------- Shape Utils ------------------ */
@@ -982,7 +990,7 @@ export class Editor extends EventEmitter<TLEventMap> {
982
990
  *
983
991
  * @public
984
992
  */
985
- shapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }
993
+ shapeUtils: { readonly [K in string]?: ShapeUtil<TLShape> }
986
994
 
987
995
  styleProps: { [key: string]: Map<StyleProp<any>, string> }
988
996
 
@@ -1001,8 +1009,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1001
1009
  *
1002
1010
  * @public
1003
1011
  */
1004
- getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>
1005
- getShapeUtil<S extends TLUnknownShape>(type: S['type']): ShapeUtil<S>
1012
+ getShapeUtil<K extends TLShape['type']>(type: K): ShapeUtil<Extract<TLShape, { type: K }>>
1013
+ getShapeUtil<S extends TLShape>(shape: S | TLShapePartial<S> | S['type']): ShapeUtil<S>
1006
1014
  getShapeUtil<T extends ShapeUtil>(type: T extends ShapeUtil<infer R> ? R['type'] : string): T
1007
1015
  getShapeUtil(arg: string | { type: string }) {
1008
1016
  const type = typeof arg === 'string' ? arg : arg.type
@@ -1016,8 +1024,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1016
1024
  *
1017
1025
  * @param shape - A shape, shape partial, or shape type.
1018
1026
  */
1019
- hasShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): boolean
1020
- hasShapeUtil<S extends TLUnknownShape>(type: S['type']): boolean
1027
+ hasShapeUtil(shape: TLShape | TLShapePartial<TLShape>): boolean
1028
+ hasShapeUtil(type: TLShape['type']): boolean
1021
1029
  hasShapeUtil<T extends ShapeUtil>(
1022
1030
  type: T extends ShapeUtil<infer R> ? R['type'] : string
1023
1031
  ): boolean
@@ -1032,7 +1040,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1032
1040
  *
1033
1041
  * @public
1034
1042
  */
1035
- bindingUtils: { readonly [K in string]?: BindingUtil<TLUnknownBinding> }
1043
+ bindingUtils: { readonly [K in string]?: BindingUtil<TLBinding> }
1036
1044
 
1037
1045
  /**
1038
1046
  * Get a binding util from a binding itself.
@@ -1049,8 +1057,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1049
1057
  *
1050
1058
  * @public
1051
1059
  */
1052
- getBindingUtil<S extends TLUnknownBinding>(binding: S | { type: S['type'] }): BindingUtil<S>
1053
- getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>
1060
+ getBindingUtil<K extends TLBinding['type']>(type: K): BindingUtil<Extract<TLBinding, { type: K }>>
1061
+ getBindingUtil<S extends TLBinding>(binding: S | { type: S['type'] }): BindingUtil<S>
1054
1062
  getBindingUtil<T extends BindingUtil>(
1055
1063
  type: T extends BindingUtil<infer R> ? R['type'] : string
1056
1064
  ): T
@@ -1064,7 +1072,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1064
1072
  /* --------------------- History -------------------- */
1065
1073
 
1066
1074
  /**
1067
- * A manager for the app's history.
1075
+ * A manager for the editor's history.
1068
1076
  *
1069
1077
  * @readonly
1070
1078
  */
@@ -1088,14 +1096,18 @@ export class Editor extends EventEmitter<TLEventMap> {
1088
1096
  }
1089
1097
 
1090
1098
  /**
1091
- * Whether the app can undo.
1099
+ * Whether the editor can undo.
1092
1100
  *
1093
1101
  * @public
1094
1102
  */
1095
- @computed getCanUndo(): boolean {
1103
+ @computed canUndo(): boolean {
1096
1104
  return this.history.getNumUndos() > 0
1097
1105
  }
1098
1106
 
1107
+ getCanUndo() {
1108
+ return this.canUndo()
1109
+ }
1110
+
1099
1111
  /**
1100
1112
  * Redo to the next mark.
1101
1113
  *
@@ -1113,20 +1125,24 @@ export class Editor extends EventEmitter<TLEventMap> {
1113
1125
  return this
1114
1126
  }
1115
1127
 
1116
- clearHistory() {
1117
- this.history.clear()
1118
- return this
1119
- }
1120
-
1121
1128
  /**
1122
- * Whether the app can redo.
1129
+ * Whether the editor can redo.
1123
1130
  *
1124
1131
  * @public
1125
1132
  */
1126
- @computed getCanRedo(): boolean {
1133
+ @computed canRedo(): boolean {
1127
1134
  return this.history.getNumRedos() > 0
1128
1135
  }
1129
1136
 
1137
+ getCanRedo() {
1138
+ return this.canRedo()
1139
+ }
1140
+
1141
+ clearHistory() {
1142
+ this.history.clear()
1143
+ return this
1144
+ }
1145
+
1130
1146
  /**
1131
1147
  * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
1132
1148
  * any redos. You typically want to do this just before a user interaction begins or is handled.
@@ -1300,7 +1316,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1300
1316
  }),
1301
1317
  selectionCount: this.getSelectedShapes().length,
1302
1318
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1303
- inputs: this.inputs,
1319
+ inputs: this.inputs.toJson(),
1304
1320
  pageState: this.getCurrentPageState(),
1305
1321
  instanceState: this.getInstanceState(),
1306
1322
  collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
@@ -1325,7 +1341,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1325
1341
  * we're in a transaction that's about to be rolled back due to the same error we're currently
1326
1342
  * reporting.
1327
1343
  *
1328
- * Instead, to listen to changes to this value, you need to listen to app's `crash` event.
1344
+ * Instead, to listen to changes to this value, you need to listen to editor's `crash` event.
1329
1345
  *
1330
1346
  * @internal
1331
1347
  */
@@ -2028,7 +2044,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2028
2044
  }
2029
2045
 
2030
2046
  /**
2031
- * The id of the app's only selected shape.
2047
+ * The id of the editor's only selected shape.
2032
2048
  *
2033
2049
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
2034
2050
  *
@@ -2040,7 +2056,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2040
2056
  }
2041
2057
 
2042
2058
  /**
2043
- * The app's only selected shape.
2059
+ * The editor's only selected shape.
2044
2060
  *
2045
2061
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
2046
2062
  *
@@ -2220,7 +2236,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2220
2236
  throw Error(`Editor.setFocusedGroup: Shape with id ${id} does not exist`)
2221
2237
  }
2222
2238
 
2223
- if (!this.isShapeOfType<TLGroupShape>(shape, 'group')) {
2239
+ if (!this.isShapeOfType(shape, 'group')) {
2224
2240
  throw Error(
2225
2241
  `Editor.setFocusedGroup: Cannot set focused group to shape of type ${shape.type}`
2226
2242
  )
@@ -2248,7 +2264,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2248
2264
  if (focusedGroup) {
2249
2265
  // If we have a focused layer, look for an ancestor of the focused shape that is a group
2250
2266
  const match = this.findShapeAncestor(focusedGroup, (shape) =>
2251
- this.isShapeOfType<TLGroupShape>(shape, 'group')
2267
+ this.isShapeOfType(shape, 'group')
2252
2268
  )
2253
2269
  // If we have an ancestor that can become a focused layer, set it as the focused layer
2254
2270
  this.setFocusedGroup(match?.id ?? null)
@@ -2281,6 +2297,29 @@ export class Editor extends EventEmitter<TLEventMap> {
2281
2297
  return editingShapeId ? this.getShape(editingShapeId) : undefined
2282
2298
  }
2283
2299
 
2300
+ /**
2301
+ * Whether the shape can be edited.
2302
+ *
2303
+ * @param shape - The shape (or shape id) to check if it can be edited.
2304
+ * @param info - The info about the edit start.
2305
+ *
2306
+ * @public
2307
+ * @returns true if the shape can be edited, false otherwise.
2308
+ */
2309
+ canEditShape<T extends TLShape | TLShapeId>(shape: T | null, info?: TLEditStartInfo): shape is T {
2310
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2311
+ if (!id) return false // no shape
2312
+ if (id === this.getEditingShapeId()) return false // already editing this shape
2313
+ const _shape = this.getShape(id)
2314
+ if (!_shape) return false // no shape
2315
+ const util = this.getShapeUtil(_shape)
2316
+ const _info: TLEditStartInfo = info ?? { type: 'unknown' }
2317
+ if (!util.canEdit(_shape, _info)) return false // shape is not editable
2318
+ if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception
2319
+ if (this.isShapeOrAncestorLocked(_shape) && !util.canEditWhileLocked(_shape)) return false // locked and no exception. Note here: we're not distinguishing between a locked shape and a shape that is the descendant of a locked shape.
2320
+ return true // shape is editable
2321
+ }
2322
+
2284
2323
  /**
2285
2324
  * Set the current editing shape.
2286
2325
  *
@@ -2296,44 +2335,59 @@ export class Editor extends EventEmitter<TLEventMap> {
2296
2335
  */
2297
2336
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2298
2337
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2299
- this.setRichTextEditor(null)
2300
- const prevEditingShapeId = this.getEditingShapeId()
2301
- if (id !== prevEditingShapeId) {
2302
- if (id) {
2303
- const shape = this.getShape(id)
2304
- if (shape && this.getShapeUtil(shape).canEdit(shape)) {
2305
- this.run(
2306
- () => {
2307
- this._updateCurrentPageState({ editingShapeId: id })
2308
- if (prevEditingShapeId) {
2309
- const prevEditingShape = this.getShape(prevEditingShapeId)
2310
- if (prevEditingShape) {
2311
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2312
- }
2313
- }
2314
- this.getShapeUtil(shape).onEditStart?.(shape)
2315
- },
2316
- { history: 'ignore' }
2317
- )
2318
- return this
2319
- }
2320
- }
2321
2338
 
2322
- // Either we just set the editing id to null, or the shape was missing or not editable
2339
+ if (!id) {
2340
+ // setting the editing shape to null
2323
2341
  this.run(
2324
2342
  () => {
2325
- this._updateCurrentPageState({ editingShapeId: null })
2326
- this._currentRichTextEditor.set(null)
2343
+ // Clean up the previous editing shape
2344
+ const prevEditingShapeId = this.getEditingShapeId()
2327
2345
  if (prevEditingShapeId) {
2328
2346
  const prevEditingShape = this.getShape(prevEditingShapeId)
2329
2347
  if (prevEditingShape) {
2330
2348
  this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2331
2349
  }
2332
2350
  }
2351
+
2352
+ // Clean up the editing shape state and rich text editor
2353
+ this._updateCurrentPageState({ editingShapeId: null })
2354
+ this._currentRichTextEditor.set(null)
2333
2355
  },
2334
2356
  { history: 'ignore' }
2335
2357
  )
2358
+
2359
+ return this
2336
2360
  }
2361
+
2362
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2363
+ if (!this.canEditShape(id)) return this
2364
+
2365
+ // id was provided and the next editing shape is editable, so set the rich text editor to null
2366
+ this.run(
2367
+ () => {
2368
+ // Clean up the previous editing shape
2369
+ const prevEditingShapeId = this.getEditingShapeId()
2370
+ if (prevEditingShapeId) {
2371
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2372
+ if (prevEditingShape) {
2373
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2374
+ }
2375
+ }
2376
+
2377
+ // Clean up the editing shape state and rich text editor
2378
+ this._updateCurrentPageState({ editingShapeId: null })
2379
+ this._currentRichTextEditor.set(null)
2380
+
2381
+ // Set the new editing shape
2382
+ this.select(id)
2383
+ this._updateCurrentPageState({ editingShapeId: id })
2384
+
2385
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2386
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2387
+ },
2388
+ { history: 'ignore' }
2389
+ )
2390
+
2337
2391
  return this
2338
2392
  }
2339
2393
 
@@ -2537,6 +2591,26 @@ export class Editor extends EventEmitter<TLEventMap> {
2537
2591
  return this.getCurrentPageState().croppingShapeId
2538
2592
  }
2539
2593
 
2594
+ /**
2595
+ * Whether the shape can be cropped.
2596
+ *
2597
+ * @param shape - The shape (or shape id) to check if it can be cropped.
2598
+ *
2599
+ * @public
2600
+ * @returns true if the shape can be cropped, false otherwise.
2601
+ */
2602
+ canCropShape<T extends TLShape | TLShapeId>(shape: T | null): shape is T {
2603
+ if (!shape) return false
2604
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2605
+ if (!id) return false
2606
+ const _shape = this.getShape(id)
2607
+ if (!_shape) return false
2608
+ const util = this.getShapeUtil(_shape)
2609
+ if (!util.canCrop(_shape)) return false
2610
+ if (this.isShapeOrAncestorLocked(_shape)) return false
2611
+ return true
2612
+ }
2613
+
2540
2614
  /**
2541
2615
  * Set the current cropping shape.
2542
2616
  *
@@ -2558,12 +2632,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2558
2632
  () => {
2559
2633
  if (!id) {
2560
2634
  this.updateCurrentPageState({ croppingShapeId: null })
2561
- } else {
2562
- const shape = this.getShape(id)!
2563
- const util = this.getShapeUtil(shape)
2564
- if (shape && util.canCrop(shape)) {
2565
- this.updateCurrentPageState({ croppingShapeId: id })
2566
- }
2635
+ } else if (this.canCropShape(id)) {
2636
+ this.updateCurrentPageState({ croppingShapeId: id })
2567
2637
  }
2568
2638
  },
2569
2639
  { history: 'ignore' }
@@ -2673,6 +2743,52 @@ export class Editor extends EventEmitter<TLEventMap> {
2673
2743
  return this.getCamera().z
2674
2744
  }
2675
2745
 
2746
+ private _debouncedZoomLevel = atom('debounced zoom level', 1)
2747
+
2748
+ /**
2749
+ * Get the debounced zoom level. When the camera is moving, this returns the zoom level
2750
+ * from when the camera started moving rather than the current zoom level. This can be
2751
+ * used to avoid expensive re-renders during camera movements.
2752
+ *
2753
+ * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom`
2754
+ * is `false`, this method always returns the current zoom level.
2755
+ *
2756
+ * @public
2757
+ */
2758
+ @computed getDebouncedZoomLevel() {
2759
+ if (this.options.debouncedZoom) {
2760
+ if (this.getCameraState() === 'idle') {
2761
+ return this.getZoomLevel()
2762
+ } else {
2763
+ return this._debouncedZoomLevel.get()
2764
+ }
2765
+ }
2766
+
2767
+ return this.getZoomLevel()
2768
+ }
2769
+
2770
+ @computed private _getAboveDebouncedZoomThreshold() {
2771
+ return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold
2772
+ }
2773
+
2774
+ /**
2775
+ * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2776
+ * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2777
+ *
2778
+ * @public
2779
+ * @example
2780
+ * ```ts
2781
+ * editor.getEfficientZoomLevel()
2782
+ * ```
2783
+ *
2784
+ * @public
2785
+ */
2786
+ @computed getEfficientZoomLevel() {
2787
+ return this._getAboveDebouncedZoomThreshold()
2788
+ ? this.getDebouncedZoomLevel()
2789
+ : this.getZoomLevel()
2790
+ }
2791
+
2676
2792
  /**
2677
2793
  * Get the camera's initial or reset zoom level.
2678
2794
  *
@@ -2999,7 +3115,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2999
3115
 
3000
3116
  // Dispatch a new pointer move because the pointer's page will have changed
3001
3117
  // (its screen position will compute to a new page position given the new camera position)
3002
- const { currentScreenPoint, currentPagePoint } = this.inputs
3118
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
3119
+ const currentPagePoint = this.inputs.getCurrentPagePoint()
3003
3120
 
3004
3121
  // compare the next page point (derived from the current camera) to the current page point
3005
3122
  if (
@@ -3163,7 +3280,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3163
3280
  * ```ts
3164
3281
  * editor.zoomIn()
3165
3282
  * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
3166
- * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
3283
+ * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
3167
3284
  * ```
3168
3285
  *
3169
3286
  * @param point - The screen point to zoom in on. Defaults to the screen center
@@ -3208,7 +3325,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3208
3325
  * ```ts
3209
3326
  * editor.zoomOut()
3210
3327
  * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
3211
- * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
3328
+ * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } })
3212
3329
  * ```
3213
3330
  *
3214
3331
  * @param point - The point to zoom out on. Defaults to the viewport screen center.
@@ -3265,10 +3382,17 @@ export class Editor extends EventEmitter<TLEventMap> {
3265
3382
 
3266
3383
  const selectionPageBounds = this.getSelectionPageBounds()
3267
3384
  if (selectionPageBounds) {
3268
- this.zoomToBounds(selectionPageBounds, {
3269
- targetZoom: Math.max(1, this.getZoomLevel()),
3270
- ...opts,
3271
- })
3385
+ const currentZoom = this.getZoomLevel()
3386
+ // If already at 100%, zoom to fit the selection in the viewport
3387
+ // Otherwise, zoom to 100% centered on the selection
3388
+ if (Math.abs(currentZoom - 1) < 0.01) {
3389
+ this.zoomToBounds(selectionPageBounds, opts)
3390
+ } else {
3391
+ this.zoomToBounds(selectionPageBounds, {
3392
+ targetZoom: 1,
3393
+ ...opts,
3394
+ })
3395
+ }
3272
3396
  }
3273
3397
  return this
3274
3398
  }
@@ -3325,7 +3449,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3325
3449
 
3326
3450
  const viewportScreenBounds = this.getViewportScreenBounds()
3327
3451
 
3328
- const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28)
3452
+ const inset =
3453
+ opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28)
3329
3454
 
3330
3455
  const baseZoom = this.getBaseZoom()
3331
3456
  const zoomMin = cameraOptions.zoomSteps[0]
@@ -3635,22 +3760,23 @@ export class Editor extends EventEmitter<TLEventMap> {
3635
3760
  if (_willSetInitialBounds) {
3636
3761
  // If we have just received the initial bounds, don't center the camera.
3637
3762
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3763
+ this.emit('resize', screenBounds.toJson())
3638
3764
  this.setCamera(this.getCamera())
3639
3765
  } else {
3640
3766
  if (center && !this.getInstanceState().followingUserId) {
3641
3767
  // Get the page center before the change, make the change, and restore it
3642
3768
  const before = this.getViewportPageBounds().center
3643
3769
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3770
+ this.emit('resize', screenBounds.toJson())
3644
3771
  this.centerOnPoint(before)
3645
3772
  } else {
3646
3773
  // Otherwise,
3647
3774
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3775
+ this.emit('resize', screenBounds.toJson())
3648
3776
  this._setCamera(Vec.From({ ...this.getCamera() }))
3649
3777
  }
3650
3778
  }
3651
3779
 
3652
- this._tickCameraState()
3653
-
3654
3780
  return this
3655
3781
  }
3656
3782
 
@@ -4056,18 +4182,19 @@ export class Editor extends EventEmitter<TLEventMap> {
4056
4182
  // box just for rendering, and we only update after the camera stops moving.
4057
4183
  private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4058
4184
  private _cameraStateTimeoutRemaining = 0
4059
- _decayCameraStateTimeout(elapsed: number) {
4185
+ private _decayCameraStateTimeout(elapsed: number) {
4060
4186
  this._cameraStateTimeoutRemaining -= elapsed
4061
4187
  if (this._cameraStateTimeoutRemaining > 0) return
4062
4188
  this.off('tick', this._decayCameraStateTimeout)
4063
4189
  this._cameraState.set('idle')
4064
4190
  }
4065
- _tickCameraState() {
4191
+ private _tickCameraState() {
4066
4192
  // always reset the timeout
4067
4193
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4068
4194
  // If the state is idle, then start the tick
4069
4195
  if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4070
4196
  this._cameraState.set('moving')
4197
+ this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4071
4198
  this.on('tick', this._decayCameraStateTimeout)
4072
4199
  }
4073
4200
 
@@ -5014,6 +5141,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5014
5141
  }
5015
5142
 
5016
5143
  private _notVisibleShapes = notVisibleShapes(this)
5144
+ private _culledShapesCache: Set<TLShapeId> | null = null
5017
5145
 
5018
5146
  /**
5019
5147
  * Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
@@ -5025,16 +5153,41 @@ export class Editor extends EventEmitter<TLEventMap> {
5025
5153
  const notVisibleShapes = this.getNotVisibleShapes()
5026
5154
  const selectedShapeIds = this.getSelectedShapeIds()
5027
5155
  const editingId = this.getEditingShapeId()
5028
- const culledShapes = new Set<TLShapeId>(notVisibleShapes)
5156
+ const nextValue = new Set<TLShapeId>(notVisibleShapes)
5029
5157
  // we don't cull the shape we are editing
5030
5158
  if (editingId) {
5031
- culledShapes.delete(editingId)
5159
+ nextValue.delete(editingId)
5032
5160
  }
5033
5161
  // we also don't cull selected shapes
5034
5162
  selectedShapeIds.forEach((id) => {
5035
- culledShapes.delete(id)
5163
+ nextValue.delete(id)
5036
5164
  })
5037
- return culledShapes
5165
+
5166
+ // Cache optimization: return same Set object if contents unchanged
5167
+ // This allows consumers to use === comparison and prevents unnecessary re-renders
5168
+ const prevValue = this._culledShapesCache
5169
+ if (prevValue) {
5170
+ // If sizes differ, contents must differ
5171
+ if (prevValue.size !== nextValue.size) {
5172
+ this._culledShapesCache = nextValue
5173
+ return nextValue
5174
+ }
5175
+
5176
+ // Check if all elements are the same
5177
+ for (const id of prevValue) {
5178
+ if (!nextValue.has(id)) {
5179
+ // Found a difference, update cache and return new set
5180
+ this._culledShapesCache = nextValue
5181
+ return nextValue
5182
+ }
5183
+ }
5184
+
5185
+ // Loop completed without finding differences - contents identical
5186
+ return prevValue
5187
+ }
5188
+
5189
+ this._culledShapesCache = nextValue
5190
+ return nextValue
5038
5191
  }
5039
5192
 
5040
5193
  /**
@@ -5101,11 +5254,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5101
5254
  let inMarginClosestToEdgeDistance = Infinity
5102
5255
  let inMarginClosestToEdgeHit: TLShape | null = null
5103
5256
 
5257
+ // Use larger margin for spatial search to account for edge distance checks
5258
+ const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
5259
+ const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5260
+
5104
5261
  const shapesToCheck = (
5105
5262
  opts.renderingOnly
5106
5263
  ? this.getCurrentPageRenderingShapesSorted()
5107
5264
  : this.getCurrentPageShapesSorted()
5108
5265
  ).filter((shape) => {
5266
+ // Frames have labels positioned above the shape (outside bounds), so always include them
5267
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5268
+
5109
5269
  if (
5110
5270
  (shape.isLocked && !hitLocked) ||
5111
5271
  this.isShapeHidden(shape) ||
@@ -5127,10 +5287,10 @@ export class Editor extends EventEmitter<TLEventMap> {
5127
5287
 
5128
5288
  // Check labels first
5129
5289
  if (
5130
- this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
5131
- ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
5132
- this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
5133
- (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
5290
+ this.isShapeOfType(shape, 'frame') ||
5291
+ ((this.isShapeOfType(shape, 'note') ||
5292
+ this.isShapeOfType(shape, 'arrow') ||
5293
+ (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5134
5294
  this.getShapeUtil(shape).getText(shape)?.trim())
5135
5295
  ) {
5136
5296
  for (const childGeometry of (geometry as Group2d).children) {
@@ -5140,7 +5300,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5140
5300
  }
5141
5301
  }
5142
5302
 
5143
- if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5303
+ if (this.isShapeOfType(shape, 'frame')) {
5144
5304
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5145
5305
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5146
5306
 
@@ -5291,11 +5451,41 @@ export class Editor extends EventEmitter<TLEventMap> {
5291
5451
  point: VecLike,
5292
5452
  opts = {} as { margin?: number; hitInside?: boolean }
5293
5453
  ): TLShape[] {
5454
+ const margin = opts.margin ?? 0
5455
+ const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
5456
+
5457
+ // Get all page shapes in z-index order and filter to candidates that pass isPointInShape
5458
+ // Frames are always checked because their labels can be outside their bounds
5294
5459
  return this.getCurrentPageShapesSorted()
5295
- .filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
5460
+ .filter((shape) => {
5461
+ if (this.isShapeHidden(shape)) return false
5462
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5463
+ return this.isPointInShape(shape, point, opts)
5464
+ })
5296
5465
  .reverse()
5297
5466
  }
5298
5467
 
5468
+ /**
5469
+ * Get shape IDs within the given bounds.
5470
+ *
5471
+ * Note: Uses shape page bounds only. Frames with labels outside their bounds
5472
+ * may not be included even if the label is within the search bounds.
5473
+ *
5474
+ * Note: Results are unordered. If you need z-order, combine with sorted shapes:
5475
+ * ```ts
5476
+ * const candidates = editor.getShapeIdsInsideBounds(bounds)
5477
+ * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
5478
+ * ```
5479
+ *
5480
+ * @param bounds - The bounds to search within.
5481
+ * @returns Unordered set of shape IDs within the given bounds.
5482
+ *
5483
+ * @internal
5484
+ */
5485
+ getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
5486
+ return this._spatialIndex.getShapeIdsInsideBounds(bounds)
5487
+ }
5488
+
5299
5489
  /**
5300
5490
  * Test whether a point (in the current page space) will will a shape. This method takes into account masks,
5301
5491
  * such as when a shape is the child of a frame and is partially clipped by the frame.
@@ -5421,7 +5611,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5421
5611
  *
5422
5612
  * @example
5423
5613
  * ```ts
5424
- * const isArrowShape = isShapeOfType<TLArrowShape>(someShape, 'arrow')
5614
+ * const isArrowShape = isShapeOfType(someShape, 'arrow')
5425
5615
  * ```
5426
5616
  *
5427
5617
  * @param util - the TLShapeUtil constructor to test against
@@ -5429,15 +5619,16 @@ export class Editor extends EventEmitter<TLEventMap> {
5429
5619
  *
5430
5620
  * @public
5431
5621
  */
5432
- isShapeOfType<T extends TLUnknownShape>(shape: TLUnknownShape, type: T['type']): shape is T
5433
- isShapeOfType<T extends TLUnknownShape>(
5434
- shapeId: TLUnknownShape['id'],
5435
- type: T['type']
5436
- ): shapeId is T['id']
5437
- isShapeOfType<T extends TLUnknownShape>(
5438
- arg: TLUnknownShape | TLUnknownShape['id'],
5622
+ isShapeOfType<K extends TLShape['type']>(
5623
+ shape: TLShape,
5624
+ type: K
5625
+ ): shape is Extract<TLShape, { type: K }>
5626
+ isShapeOfType<T extends TLShape>(
5627
+ shape: TLShape,
5439
5628
  type: T['type']
5440
- ) {
5629
+ ): shape is Extract<TLShape, { type: T['type'] }>
5630
+ isShapeOfType<T extends TLShape = TLShape>(shapeId: TLShapeId, type: T['type']): boolean
5631
+ isShapeOfType(arg: TLShape | TLShapeId, type: TLShape['type']) {
5441
5632
  const shape = typeof arg === 'string' ? this.getShape(arg) : arg
5442
5633
  if (!shape) return false
5443
5634
  return shape.type === type
@@ -5833,7 +6024,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5833
6024
 
5834
6025
  while (node) {
5835
6026
  if (
5836
- this.isShapeOfType<TLGroupShape>(node, 'group') &&
6027
+ this.isShapeOfType(node, 'group') &&
5837
6028
  focusedGroup?.id !== node.id &&
5838
6029
  !this.hasAncestor(focusedGroup, node.id) &&
5839
6030
  (filter?.(node) ?? true)
@@ -5875,7 +6066,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5875
6066
  * Get all bindings of a certain type _from_ a particular shape. These are the bindings whose
5876
6067
  * `fromId` matched the shape's ID.
5877
6068
  */
5878
- getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(
6069
+ getBindingsFromShape<K extends TLBinding['type']>(
6070
+ shape: TLShape | TLShapeId,
6071
+ type: K
6072
+ ): Extract<TLBinding, { type: K }>[]
6073
+ getBindingsFromShape<Binding extends TLBinding = TLBinding>(
6074
+ shape: TLShape | TLShapeId,
6075
+ type: Binding['type']
6076
+ ): Binding[]
6077
+ getBindingsFromShape<Binding extends TLBinding = TLBinding>(
5879
6078
  shape: TLShape | TLShapeId,
5880
6079
  type: Binding['type']
5881
6080
  ): Binding[] {
@@ -5889,7 +6088,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5889
6088
  * Get all bindings of a certain type _to_ a particular shape. These are the bindings whose
5890
6089
  * `toId` matches the shape's ID.
5891
6090
  */
5892
- getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(
6091
+ getBindingsToShape<K extends TLBinding['type']>(
6092
+ shape: TLShape | TLShapeId,
6093
+ type: K
6094
+ ): Extract<TLBinding, { type: K }>[]
6095
+ getBindingsToShape<Binding extends TLBinding = TLBinding>(
6096
+ shape: TLShape | TLShapeId,
6097
+ type: Binding['type']
6098
+ ): Binding[]
6099
+ getBindingsToShape<Binding extends TLBinding = TLBinding>(
5893
6100
  shape: TLShape | TLShapeId,
5894
6101
  type: Binding['type']
5895
6102
  ): Binding[] {
@@ -5903,7 +6110,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5903
6110
  * Get all bindings involving a particular shape. This includes bindings where the shape is the
5904
6111
  * `fromId` or `toId`. If a type is provided, only bindings of that type are returned.
5905
6112
  */
5906
- getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(
6113
+ getBindingsInvolvingShape<K extends TLBinding['type']>(
6114
+ shape: TLShape | TLShapeId,
6115
+ type: K
6116
+ ): Extract<TLBinding, { type: K }>[]
6117
+ getBindingsInvolvingShape<Binding extends TLBinding = TLBinding>(
6118
+ shape: TLShape | TLShapeId,
6119
+ type?: Binding['type']
6120
+ ): Binding[]
6121
+ getBindingsInvolvingShape<Binding extends TLBinding = TLBinding>(
5907
6122
  shape: TLShape | TLShapeId,
5908
6123
  type?: Binding['type']
5909
6124
  ): Binding[] {
@@ -5925,7 +6140,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5925
6140
  if (!fromShape || !toShape) continue
5926
6141
  if (!this.canBindShapes({ fromShape, toShape, binding: partial })) continue
5927
6142
 
5928
- const util = this.getBindingUtil<TLUnknownBinding>(partial.type)
6143
+ const util = this.getBindingUtil(partial.type)
5929
6144
  const defaultProps = util.getDefaultProps()
5930
6145
  const binding = this.store.schema.types.binding.create({
5931
6146
  ...partial,
@@ -6030,7 +6245,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6030
6245
  const toShapeType = typeof toShape === 'string' ? toShape : toShape.type
6031
6246
  const bindingType = typeof binding === 'string' ? binding : binding.type
6032
6247
 
6033
- const canBindOpts = { fromShapeType, toShapeType, bindingType }
6248
+ const canBindOpts = { fromShapeType, toShapeType, bindingType } as const
6034
6249
 
6035
6250
  if (fromShapeType === toShapeType) {
6036
6251
  return this.getShapeUtil(fromShapeType).canBind(canBindOpts)
@@ -6571,7 +6786,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6571
6786
  const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
6572
6787
 
6573
6788
  for (const shape of shapesToFlipFirstPass) {
6574
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
6789
+ if (this.isShapeOfType(shape, 'group')) {
6575
6790
  const childrenOfGroups = compact(
6576
6791
  this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
6577
6792
  )
@@ -7628,8 +7843,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7628
7843
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
7629
7844
  // to make sure the shape is mirrored correctly
7630
7845
  if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
7631
- let { rotation } = Mat.Decompose(options.initialPageTransform)
7632
- rotation -= 2 * rotation
7846
+ // We need to compute the new local rotation that will result in the negated page rotation.
7847
+ // For a shape with local rotation `localRot` and parent page rotation `parentRot`:
7848
+ // - pageRot = parentRot + localRot
7849
+ // - newPageRot = -pageRot (we want to negate the page rotation)
7850
+ // - newPageRot = parentRot + newLocalRot (parent hasn't changed)
7851
+ // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot
7852
+ const parentRotation = this.getShapeParentTransform(id).rotation()
7853
+ const rotation = -options.initialShape.rotation - 2 * parentRotation
7633
7854
  this.updateShapes([{ id, type, rotation }])
7634
7855
  }
7635
7856
 
@@ -7649,9 +7870,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7649
7870
  )
7650
7871
 
7651
7872
  // now calculate how far away the shape is from where it needs to be
7652
- const pageBounds = this.getShapePageBounds(id)!
7653
7873
  const pageTransform = this.getShapePageTransform(id)!
7654
- const currentPageCenter = pageBounds.center
7874
+ // We need to use the local bounds center transformed to page space, not the axis-aligned
7875
+ // page bounds center. This is because the page bounds are axis-aligned and their center
7876
+ // changes when the rotation changes, but we want to use the same reference point as
7877
+ // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform).
7878
+ const currentLocalBounds = this.getShapeGeometry(id).bounds
7879
+ const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
7655
7880
  const shapePageTransformOrigin = pageTransform.point()
7656
7881
  if (!currentPageCenter || !shapePageTransformOrigin) return this
7657
7882
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
@@ -7692,9 +7917,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7692
7917
  *
7693
7918
  * @public
7694
7919
  */
7695
- canCreateShape<T extends TLUnknownShape>(
7696
- shape: OptionalKeys<TLShapePartial<T>, 'id'> | T['id']
7697
- ): boolean {
7920
+ canCreateShape(shape: OptionalKeys<TLShapePartial<TLShape>, 'id'> | TLShape['id']): boolean {
7698
7921
  return this.canCreateShapes([shape])
7699
7922
  }
7700
7923
 
@@ -7705,8 +7928,8 @@ export class Editor extends EventEmitter<TLEventMap> {
7705
7928
  *
7706
7929
  * @public
7707
7930
  */
7708
- canCreateShapes<T extends TLUnknownShape>(
7709
- shapes: (T['id'] | OptionalKeys<TLShapePartial<T>, 'id'>)[]
7931
+ canCreateShapes(
7932
+ shapes: (TLShape['id'] | OptionalKeys<TLShapePartial<TLShape>, 'id'>)[]
7710
7933
  ): boolean {
7711
7934
  return shapes.length + this.getCurrentPageShapeIds().size <= this.options.maxShapesPerPage
7712
7935
  }
@@ -7724,7 +7947,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7724
7947
  *
7725
7948
  * @public
7726
7949
  */
7727
- createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this {
7950
+ createShape<TShape extends TLShape>(shape: TLCreateShapePartial<TShape>): this {
7728
7951
  this.createShapes([shape])
7729
7952
  return this
7730
7953
  }
@@ -7742,7 +7965,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7742
7965
  *
7743
7966
  * @public
7744
7967
  */
7745
- createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this {
7968
+ createShapes<TShape extends TLShape = TLShape>(shapes: TLCreateShapePartial<TShape>[]): this {
7746
7969
  if (!Array.isArray(shapes)) {
7747
7970
  throw Error('Editor.createShapes: must provide an array of shapes or shape partials')
7748
7971
  }
@@ -8101,7 +8324,12 @@ export class Editor extends EventEmitter<TLEventMap> {
8101
8324
  )
8102
8325
  )
8103
8326
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
8104
- const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
8327
+ const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
8328
+ const pageBounds = Box.Common(childBounds)
8329
+
8330
+ if (!pageBounds.isValid()) {
8331
+ throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`)
8332
+ }
8105
8333
 
8106
8334
  const { x, y } = pageBounds.point
8107
8335
 
@@ -8123,7 +8351,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8123
8351
  const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index
8124
8352
 
8125
8353
  this.run(() => {
8126
- this.createShapes<TLGroupShape>([
8354
+ this.createShapes([
8127
8355
  {
8128
8356
  id: groupId,
8129
8357
  type: 'group',
@@ -8193,7 +8421,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8193
8421
  const groups: TLGroupShape[] = []
8194
8422
 
8195
8423
  shapesToUngroup.forEach((shape) => {
8196
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8424
+ if (this.isShapeOfType(shape, 'group')) {
8197
8425
  groups.push(shape)
8198
8426
  } else {
8199
8427
  idsToSelect.add(shape.id)
@@ -8239,7 +8467,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8239
8467
  *
8240
8468
  * @public
8241
8469
  */
8242
- updateShape<T extends TLUnknownShape>(partial: TLShapePartial<T> | null | undefined) {
8470
+ updateShape<T extends TLShape = TLShape>(partial: TLShapePartial<T> | null | undefined) {
8243
8471
  this.updateShapes([partial])
8244
8472
  return this
8245
8473
  }
@@ -8256,7 +8484,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8256
8484
  *
8257
8485
  * @public
8258
8486
  */
8259
- updateShapes<T extends TLUnknownShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
8487
+ updateShapes<T extends TLShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
8260
8488
  const compactedPartials: TLShapePartial<T>[] = Array(partials.length)
8261
8489
 
8262
8490
  for (let i = 0, n = partials.length; i < n; i++) {
@@ -8408,7 +8636,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8408
8636
  * @internal
8409
8637
  */
8410
8638
  private _extractSharedStyles(shape: TLShape, sharedStyleMap: SharedStyleMap) {
8411
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8639
+ if (this.isShapeOfType(shape, 'group')) {
8412
8640
  // For groups, ignore the styles of the group shape and instead include the styles of the
8413
8641
  // group's children. These are the shapes that would have their styles changed if the
8414
8642
  // user called `setStyle` on the current selection.
@@ -8528,7 +8756,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8528
8756
  // For groups, ignore the opacity of the group shape and instead include
8529
8757
  // the opacity of the group's children. These are the shapes that would have
8530
8758
  // their opacity changed if the user called `setOpacity` on the current selection.
8531
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8759
+ if (this.isShapeOfType(shape, 'group')) {
8532
8760
  for (const childId of this.getSortedChildIdsForParent(shape.id)) {
8533
8761
  addShape(childId)
8534
8762
  }
@@ -8589,7 +8817,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8589
8817
  // We can have many deep levels of grouped shape
8590
8818
  // Making a recursive function to look through all the levels
8591
8819
  const addShapeById = (shape: TLShape) => {
8592
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8820
+ if (this.isShapeOfType(shape, 'group')) {
8593
8821
  const childIds = this.getSortedChildIdsForParent(shape)
8594
8822
  for (const childId of childIds) {
8595
8823
  addShapeById(this.getShape(childId)!)
@@ -8673,7 +8901,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8673
8901
  // We can have many deep levels of grouped shape
8674
8902
  // Making a recursive function to look through all the levels
8675
8903
  const addShapeById = (shape: TLShape) => {
8676
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8904
+ if (this.isShapeOfType(shape, 'group')) {
8677
8905
  const childIds = this.getSortedChildIdsForParent(shape.id)
8678
8906
  for (const childId of childIds) {
8679
8907
  addShapeById(this.getShape(childId)!)
@@ -9098,7 +9326,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9098
9326
  for (const shape of this.getSelectedShapes()) {
9099
9327
  if (lowestDepth === 0) break
9100
9328
 
9101
- const isFrame = this.isShapeOfType<TLFrameShape>(shape, 'frame')
9329
+ const isFrame = this.isShapeOfType(shape, 'frame')
9102
9330
  const ancestors = this.getShapeAncestors(shape)
9103
9331
  if (isFrame) ancestors.push(shape)
9104
9332
 
@@ -9126,6 +9354,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9126
9354
  }
9127
9355
  }
9128
9356
 
9357
+ if (point) {
9358
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9359
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9360
+ if (rootShapesFromContent.length > 0) {
9361
+ const targetParent = this.getShapeAtPoint(point, {
9362
+ hitInside: true,
9363
+ hitFrameInside: true,
9364
+ hitLocked: true,
9365
+ filter: (shape) => {
9366
+ const util = this.getShapeUtil(shape)
9367
+ if (!util.canReceiveNewChildrenOfType) return false
9368
+ return rootShapesFromContent.every((rootShape) =>
9369
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9370
+ )
9371
+ },
9372
+ })
9373
+
9374
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9375
+ // parent under the pointer so that we don't keep using the original
9376
+ // selection's parent (which can keep shapes clipped inside frames).
9377
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9378
+ }
9379
+ }
9380
+
9129
9381
  let isDuplicating = false
9130
9382
 
9131
9383
  if (!isPageId(pasteParentId)) {
@@ -9137,8 +9389,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9137
9389
  if (rootShapeIds.length === 1) {
9138
9390
  const rootShape = shapes.find((s) => s.id === rootShapeIds[0])!
9139
9391
  if (
9140
- this.isShapeOfType<TLFrameShape>(parent, 'frame') &&
9141
- this.isShapeOfType<TLFrameShape>(rootShape, 'frame') &&
9392
+ this.isShapeOfType(parent, 'frame') &&
9393
+ this.isShapeOfType(rootShape, 'frame') &&
9142
9394
  rootShape.props.w === parent?.props.w &&
9143
9395
  rootShape.props.h === parent?.props.h
9144
9396
  ) {
@@ -9313,11 +9565,11 @@ export class Editor extends EventEmitter<TLEventMap> {
9313
9565
  const onlyRoot = rootShapes[0] as TLFrameShape
9314
9566
  // If the old bounds are in the viewport...
9315
9567
  // todo: replace frame references with shapes that can accept children
9316
- if (this.isShapeOfType<TLFrameShape>(onlyRoot, 'frame')) {
9568
+ if (this.isShapeOfType(onlyRoot, 'frame')) {
9317
9569
  while (
9318
9570
  this.getShapesAtPoint(point).some(
9319
9571
  (shape) =>
9320
- this.isShapeOfType<TLFrameShape>(shape, 'frame') &&
9572
+ this.isShapeOfType(shape, 'frame') &&
9321
9573
  shape.props.w === onlyRoot.props.w &&
9322
9574
  shape.props.h === onlyRoot.props.h
9323
9575
  )
@@ -9463,126 +9715,6 @@ export class Editor extends EventEmitter<TLEventMap> {
9463
9715
 
9464
9716
  /* --------------------- Events --------------------- */
9465
9717
 
9466
- /**
9467
- * The app's current input state.
9468
- *
9469
- * @public
9470
- */
9471
- inputs = {
9472
- /** The most recent pointer down's position in the current page space. */
9473
- originPagePoint: new Vec(),
9474
- /** The most recent pointer down's position in screen space. */
9475
- originScreenPoint: new Vec(),
9476
- /** The previous pointer position in the current page space. */
9477
- previousPagePoint: new Vec(),
9478
- /** The previous pointer position in screen space. */
9479
- previousScreenPoint: new Vec(),
9480
- /** The most recent pointer position in the current page space. */
9481
- currentPagePoint: new Vec(),
9482
- /** The most recent pointer position in screen space. */
9483
- currentScreenPoint: new Vec(),
9484
- /** A set containing the currently pressed keys. */
9485
- keys: new Set<string>(),
9486
- /** A set containing the currently pressed buttons. */
9487
- buttons: new Set<number>(),
9488
- /** Whether the input is from a pe. */
9489
- isPen: false,
9490
- /** Whether the shift key is currently pressed. */
9491
- shiftKey: false,
9492
- /** Whether the meta key is currently pressed. */
9493
- metaKey: false,
9494
- /** Whether the control or command key is currently pressed. */
9495
- ctrlKey: false,
9496
- /** Whether the alt or option key is currently pressed. */
9497
- altKey: false,
9498
- /** Whether the user is dragging. */
9499
- isDragging: false,
9500
- /** Whether the user is pointing. */
9501
- isPointing: false,
9502
- /** Whether the user is pinching. */
9503
- isPinching: false,
9504
- /** Whether the user is editing. */
9505
- isEditing: false,
9506
- /** Whether the user is panning. */
9507
- isPanning: false,
9508
- /** Whether the user is spacebar panning. */
9509
- isSpacebarPanning: false,
9510
- /** Velocity of mouse pointer, in pixels per millisecond */
9511
- pointerVelocity: new Vec(),
9512
- }
9513
-
9514
- /**
9515
- * Update the input points from a pointer, pinch, or wheel event.
9516
- *
9517
- * @param info - The event info.
9518
- */
9519
- private _updateInputsFromEvent(
9520
- info: TLPointerEventInfo | TLPinchEventInfo | TLWheelEventInfo
9521
- ): void {
9522
- const {
9523
- pointerVelocity,
9524
- previousScreenPoint,
9525
- previousPagePoint,
9526
- currentScreenPoint,
9527
- currentPagePoint,
9528
- originScreenPoint,
9529
- originPagePoint,
9530
- } = this.inputs
9531
-
9532
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
9533
- const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
9534
-
9535
- const sx = info.point.x - screenBounds.x
9536
- const sy = info.point.y - screenBounds.y
9537
- const sz = info.point.z ?? 0.5
9538
-
9539
- previousScreenPoint.setTo(currentScreenPoint)
9540
- previousPagePoint.setTo(currentPagePoint)
9541
-
9542
- // The "screen bounds" is relative to the user's actual screen.
9543
- // The "screen point" is relative to the "screen bounds";
9544
- // it will be 0,0 when its actual screen position is equal
9545
- // to screenBounds.point. This is confusing!
9546
- currentScreenPoint.set(sx, sy)
9547
- const nx = sx / cz - cx
9548
- const ny = sy / cz - cy
9549
- if (isFinite(nx) && isFinite(ny)) {
9550
- currentPagePoint.set(nx, ny, sz)
9551
- }
9552
-
9553
- this.inputs.isPen = info.type === 'pointer' && info.isPen
9554
-
9555
- // Reset velocity on pointer down, or when a pinch starts or ends
9556
- if (info.name === 'pointer_down' || this.inputs.isPinching) {
9557
- pointerVelocity.set(0, 0)
9558
- originScreenPoint.setTo(currentScreenPoint)
9559
- originPagePoint.setTo(currentPagePoint)
9560
- }
9561
-
9562
- // todo: We only have to do this if there are multiple users in the document
9563
- this.run(
9564
- () => {
9565
- this.store.put([
9566
- {
9567
- id: TLPOINTER_ID,
9568
- typeName: 'pointer',
9569
- x: currentPagePoint.x,
9570
- y: currentPagePoint.y,
9571
- lastActivityTimestamp:
9572
- // If our pointer moved only because we're following some other user, then don't
9573
- // update our last activity timestamp; otherwise, update it to the current timestamp.
9574
- info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
9575
- ? (this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
9576
- this._tickManager.now)
9577
- : this._tickManager.now,
9578
- meta: {},
9579
- },
9580
- ])
9581
- },
9582
- { history: 'ignore' }
9583
- )
9584
- }
9585
-
9586
9718
  /**
9587
9719
  * Dispatch a cancel event.
9588
9720
  *
@@ -9652,19 +9784,22 @@ export class Editor extends EventEmitter<TLEventMap> {
9652
9784
  // weird but true: what `inputs` calls screen-space is actually viewport space. so
9653
9785
  // we need to convert back into true screen space first. we should fix this...
9654
9786
  Vec.Add(
9655
- this.inputs.currentScreenPoint,
9787
+ this.inputs.getCurrentScreenPoint(),
9656
9788
  this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9657
9789
  ),
9658
9790
  pointerId: options?.pointerId ?? 0,
9659
9791
  button: options?.button ?? 0,
9660
- isPen: options?.isPen ?? this.inputs.isPen,
9661
- shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9662
- altKey: options?.altKey ?? this.inputs.altKey,
9663
- ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9664
- metaKey: options?.metaKey ?? this.inputs.metaKey,
9665
- accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9792
+ isPen: options?.isPen ?? this.inputs.getIsPen(),
9793
+ shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(),
9794
+ altKey: options?.altKey ?? this.inputs.getAltKey(),
9795
+ ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(),
9796
+ metaKey: options?.metaKey ?? this.inputs.getMetaKey(),
9797
+ accelKey: false,
9666
9798
  }
9667
9799
 
9800
+ // needs to be calculated second
9801
+ event.accelKey = options?.accelKey ?? this.inputs.getAccelKey()
9802
+
9668
9803
  if (options?.immediate) {
9669
9804
  this._flushEventForTick(event)
9670
9805
  } else {
@@ -10037,16 +10172,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10037
10172
  /** @internal */
10038
10173
  @bind
10039
10174
  _setShiftKeyTimeout() {
10040
- this.inputs.shiftKey = false
10175
+ this.inputs.setShiftKey(false)
10041
10176
  this.dispatch({
10042
10177
  type: 'keyboard',
10043
10178
  name: 'key_up',
10044
10179
  key: 'Shift',
10045
- shiftKey: this.inputs.shiftKey,
10046
- ctrlKey: this.inputs.ctrlKey,
10047
- altKey: this.inputs.altKey,
10048
- metaKey: this.inputs.metaKey,
10049
- accelKey: isAccelKey(this.inputs),
10180
+ shiftKey: this.inputs.getShiftKey(),
10181
+ ctrlKey: this.inputs.getCtrlKey(),
10182
+ altKey: this.inputs.getAltKey(),
10183
+ metaKey: this.inputs.getMetaKey(),
10184
+ accelKey: this.inputs.getAccelKey(),
10050
10185
  code: 'ShiftLeft',
10051
10186
  })
10052
10187
  }
@@ -10057,16 +10192,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10057
10192
  /** @internal */
10058
10193
  @bind
10059
10194
  _setAltKeyTimeout() {
10060
- this.inputs.altKey = false
10195
+ this.inputs.setAltKey(false)
10061
10196
  this.dispatch({
10062
10197
  type: 'keyboard',
10063
10198
  name: 'key_up',
10064
10199
  key: 'Alt',
10065
- shiftKey: this.inputs.shiftKey,
10066
- ctrlKey: this.inputs.ctrlKey,
10067
- altKey: this.inputs.altKey,
10068
- metaKey: this.inputs.metaKey,
10069
- accelKey: isAccelKey(this.inputs),
10200
+ shiftKey: this.inputs.getShiftKey(),
10201
+ ctrlKey: this.inputs.getCtrlKey(),
10202
+ altKey: this.inputs.getAltKey(),
10203
+ metaKey: this.inputs.getMetaKey(),
10204
+ accelKey: this.inputs.getAccelKey(),
10070
10205
  code: 'AltLeft',
10071
10206
  })
10072
10207
  }
@@ -10077,16 +10212,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10077
10212
  /** @internal */
10078
10213
  @bind
10079
10214
  _setCtrlKeyTimeout() {
10080
- this.inputs.ctrlKey = false
10215
+ this.inputs.setCtrlKey(false)
10081
10216
  this.dispatch({
10082
10217
  type: 'keyboard',
10083
10218
  name: 'key_up',
10084
10219
  key: 'Ctrl',
10085
- shiftKey: this.inputs.shiftKey,
10086
- ctrlKey: this.inputs.ctrlKey,
10087
- altKey: this.inputs.altKey,
10088
- metaKey: this.inputs.metaKey,
10089
- accelKey: isAccelKey(this.inputs),
10220
+ shiftKey: this.inputs.getShiftKey(),
10221
+ ctrlKey: this.inputs.getCtrlKey(),
10222
+ altKey: this.inputs.getAltKey(),
10223
+ metaKey: this.inputs.getMetaKey(),
10224
+ accelKey: this.inputs.getAccelKey(),
10090
10225
  code: 'ControlLeft',
10091
10226
  })
10092
10227
  }
@@ -10097,16 +10232,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10097
10232
  /** @internal */
10098
10233
  @bind
10099
10234
  _setMetaKeyTimeout() {
10100
- this.inputs.metaKey = false
10235
+ this.inputs.setMetaKey(false)
10101
10236
  this.dispatch({
10102
10237
  type: 'keyboard',
10103
10238
  name: 'key_up',
10104
10239
  key: 'Meta',
10105
- shiftKey: this.inputs.shiftKey,
10106
- ctrlKey: this.inputs.ctrlKey,
10107
- altKey: this.inputs.altKey,
10108
- metaKey: this.inputs.metaKey,
10109
- accelKey: isAccelKey(this.inputs),
10240
+ shiftKey: this.inputs.getShiftKey(),
10241
+ ctrlKey: this.inputs.getCtrlKey(),
10242
+ altKey: this.inputs.getAltKey(),
10243
+ metaKey: this.inputs.getMetaKey(),
10244
+ accelKey: this.inputs.getAccelKey(),
10110
10245
  code: 'MetaLeft',
10111
10246
  })
10112
10247
  }
@@ -10114,9 +10249,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10114
10249
  /** @internal */
10115
10250
  private _restoreToolId = 'select'
10116
10251
 
10117
- /** @internal */
10118
- private _pinchStart = 1
10119
-
10120
10252
  /** @internal */
10121
10253
  private _didPinch = false
10122
10254
 
@@ -10223,55 +10355,54 @@ export class Editor extends EventEmitter<TLEventMap> {
10223
10355
  if (info.type === 'misc') {
10224
10356
  // stop panning if the interaction is cancelled or completed
10225
10357
  if (info.name === 'cancel' || info.name === 'complete') {
10226
- this.inputs.isDragging = false
10358
+ this.inputs.setIsDragging(false)
10227
10359
 
10228
- if (this.inputs.isPanning) {
10229
- this.inputs.isPanning = false
10230
- this.inputs.isSpacebarPanning = false
10360
+ if (this.inputs.getIsPanning()) {
10361
+ this.inputs.setIsPanning(false)
10362
+ this.inputs.setIsSpacebarPanning(false)
10231
10363
  this.setCursor({ type: this._prevCursor, rotation: 0 })
10232
10364
  }
10233
10365
  }
10234
10366
 
10235
10367
  this.root.handleEvent(info)
10368
+ this.emit('event', info)
10236
10369
  return
10237
10370
  }
10238
10371
 
10239
10372
  if (info.shiftKey) {
10240
10373
  clearTimeout(this._shiftKeyTimeout)
10241
10374
  this._shiftKeyTimeout = -1
10242
- inputs.shiftKey = true
10243
- } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
10375
+ inputs.setShiftKey(true)
10376
+ } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
10244
10377
  this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
10245
10378
  }
10246
10379
 
10247
10380
  if (info.altKey) {
10248
10381
  clearTimeout(this._altKeyTimeout)
10249
10382
  this._altKeyTimeout = -1
10250
- inputs.altKey = true
10251
- } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
10383
+ inputs.setAltKey(true)
10384
+ } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
10252
10385
  this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
10253
10386
  }
10254
10387
 
10255
10388
  if (info.ctrlKey) {
10256
10389
  clearTimeout(this._ctrlKeyTimeout)
10257
10390
  this._ctrlKeyTimeout = -1
10258
- inputs.ctrlKey = true
10259
- } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
10391
+ inputs.setCtrlKey(true)
10392
+ } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
10260
10393
  this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
10261
10394
  }
10262
10395
 
10263
10396
  if (info.metaKey) {
10264
10397
  clearTimeout(this._metaKeyTimeout)
10265
10398
  this._metaKeyTimeout = -1
10266
- inputs.metaKey = true
10267
- } else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
10399
+ inputs.setMetaKey(true)
10400
+ } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
10268
10401
  this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150)
10269
10402
  }
10270
10403
 
10271
- const { originPagePoint, currentPagePoint } = inputs
10272
-
10273
- if (!inputs.isPointing) {
10274
- inputs.isDragging = false
10404
+ if (!inputs.getIsPointing()) {
10405
+ inputs.setIsDragging(false)
10275
10406
  }
10276
10407
 
10277
10408
  const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -10282,29 +10413,29 @@ export class Editor extends EventEmitter<TLEventMap> {
10282
10413
  case 'pinch': {
10283
10414
  if (cameraOptions.isLocked) return
10284
10415
  clearTimeout(this._longPressTimeout)
10285
- this._updateInputsFromEvent(info)
10416
+ this.inputs.updateFromEvent(info)
10286
10417
 
10287
10418
  switch (info.name) {
10288
10419
  case 'pinch_start': {
10289
- if (inputs.isPinching) return
10420
+ if (inputs.getIsPinching()) return
10290
10421
 
10291
- if (!inputs.isEditing) {
10292
- this._pinchStart = this.getCamera().z
10422
+ if (!inputs.getIsEditing()) {
10293
10423
  if (!this._selectedShapeIdsAtPointerDown.length) {
10294
10424
  this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10295
10425
  }
10296
10426
 
10297
10427
  this._didPinch = true
10298
10428
 
10299
- inputs.isPinching = true
10429
+ inputs.setIsPinching(true)
10300
10430
 
10301
10431
  this.interrupt()
10302
10432
  }
10303
10433
 
10434
+ this.emit('event', info)
10304
10435
  return // Stop here!
10305
10436
  }
10306
10437
  case 'pinch': {
10307
- if (!inputs.isPinching) return
10438
+ if (!inputs.getIsPinching()) return
10308
10439
 
10309
10440
  const {
10310
10441
  point: { z = 1 },
@@ -10335,13 +10466,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10335
10466
  { immediate: true }
10336
10467
  )
10337
10468
 
10469
+ this.emit('event', info)
10338
10470
  return // Stop here!
10339
10471
  }
10340
10472
  case 'pinch_end': {
10341
- if (!inputs.isPinching) return this
10473
+ if (!inputs.getIsPinching()) return this
10342
10474
 
10343
10475
  // Stop pinching
10344
- inputs.isPinching = false
10476
+ inputs.setIsPinching(false)
10345
10477
 
10346
10478
  // Stash and clear the shapes that were selected when the pinch started
10347
10479
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
@@ -10361,6 +10493,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10361
10493
  }
10362
10494
  }
10363
10495
 
10496
+ this.emit('event', info)
10364
10497
  return // Stop here!
10365
10498
  }
10366
10499
  }
@@ -10368,7 +10501,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10368
10501
  case 'wheel': {
10369
10502
  if (cameraOptions.isLocked) return
10370
10503
 
10371
- this._updateInputsFromEvent(info)
10504
+ this.inputs.updateFromEvent(info)
10372
10505
 
10373
10506
  const { panSpeed, zoomSpeed } = cameraOptions
10374
10507
  let wheelBehavior = cameraOptions.wheelBehavior
@@ -10399,7 +10532,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10399
10532
  switch (behavior) {
10400
10533
  case 'zoom': {
10401
10534
  // Zoom in on current screen point using the wheel delta
10402
- const { x, y } = this.inputs.currentScreenPoint
10535
+ const { x, y } = this.inputs.getCurrentScreenPoint()
10403
10536
  let delta = dz
10404
10537
 
10405
10538
  // If we're forcing zoom, then we need to do the wheel normalization math here
@@ -10416,6 +10549,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10416
10549
  immediate: true,
10417
10550
  })
10418
10551
  this.maybeTrackPerformance('Zooming')
10552
+ this.root.handleEvent(info)
10553
+ this.emit('event', info)
10419
10554
  return
10420
10555
  }
10421
10556
  case 'pan': {
@@ -10424,6 +10559,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10424
10559
  immediate: true,
10425
10560
  })
10426
10561
  this.maybeTrackPerformance('Panning')
10562
+ this.root.handleEvent(info)
10563
+ this.emit('event', info)
10427
10564
  return
10428
10565
  }
10429
10566
  }
@@ -10432,9 +10569,9 @@ export class Editor extends EventEmitter<TLEventMap> {
10432
10569
  }
10433
10570
  case 'pointer': {
10434
10571
  // Ignore pointer events while we're pinching
10435
- if (inputs.isPinching) return
10572
+ if (inputs.getIsPinching()) return
10436
10573
 
10437
- this._updateInputsFromEvent(info)
10574
+ this.inputs.updateFromEvent(info)
10438
10575
  const { isPen } = info
10439
10576
  const { isPenMode } = instanceState
10440
10577
 
@@ -10443,7 +10580,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10443
10580
  // If we're in pen mode and the input is not a pen type, then stop here
10444
10581
  if (isPenMode && !isPen) return
10445
10582
 
10446
- if (!this.inputs.isPanning) {
10583
+ if (!this.inputs.getIsPanning()) {
10447
10584
  // Start a long press timeout
10448
10585
  this._longPressTimeout = this.timers.setTimeout(() => {
10449
10586
  const vsb = this.getViewportScreenBounds()
@@ -10453,7 +10590,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10453
10590
  // viewport bounds, and will be again when this event is handled...
10454
10591
  // so we need to counter-adjust from the stored value so that the
10455
10592
  // new value is set correctly.
10456
- point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
10593
+ point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y),
10457
10594
  name: 'long_press',
10458
10595
  })
10459
10596
  }, this.options.longPressDurationMs)
@@ -10470,8 +10607,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10470
10607
  inputs.buttons.add(info.button)
10471
10608
 
10472
10609
  // Start pointing and stop dragging
10473
- inputs.isPointing = true
10474
- inputs.isDragging = false
10610
+ inputs.setIsPointing(true)
10611
+ inputs.setIsDragging(false)
10475
10612
 
10476
10613
  // If pen mode is off but we're not already in pen mode, turn that on
10477
10614
  if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
@@ -10483,16 +10620,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10483
10620
  this.setCurrentTool('eraser')
10484
10621
  } else if (info.button === MIDDLE_MOUSE_BUTTON) {
10485
10622
  // Middle mouse pan activates panning unless we're already panning (with spacebar)
10486
- if (!this.inputs.isPanning) {
10623
+ if (!this.inputs.getIsPanning()) {
10487
10624
  this._prevCursor = this.getInstanceState().cursor.type
10488
10625
  }
10489
- this.inputs.isPanning = true
10626
+ this.inputs.setIsPanning(true)
10490
10627
  clearTimeout(this._longPressTimeout)
10491
10628
  }
10492
10629
 
10493
10630
  // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click
10494
10631
  // Also stop here, we don't want the state chart to receive the event
10495
- if (this.inputs.isPanning) {
10632
+ if (this.inputs.getIsPanning()) {
10496
10633
  this.stopCameraAnimation()
10497
10634
  this.setCursor({ type: 'grabbing', rotation: 0 })
10498
10635
  return this
@@ -10507,9 +10644,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10507
10644
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
10508
10645
 
10509
10646
  // If we've started panning, then clear any long press timeout
10510
- if (this.inputs.isPanning && this.inputs.isPointing) {
10647
+ if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) {
10511
10648
  // Handle spacebar / middle mouse button panning
10512
- const { currentScreenPoint, previousScreenPoint } = this.inputs
10649
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
10650
+ const previousScreenPoint = this.inputs.getPreviousScreenPoint()
10513
10651
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10514
10652
  this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10515
10653
  immediate: true,
@@ -10519,24 +10657,25 @@ export class Editor extends EventEmitter<TLEventMap> {
10519
10657
  }
10520
10658
 
10521
10659
  if (
10522
- inputs.isPointing &&
10523
- !inputs.isDragging &&
10524
- Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() >
10660
+ inputs.getIsPointing() &&
10661
+ !inputs.getIsDragging() &&
10662
+ Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) *
10663
+ this.getZoomLevel() >
10525
10664
  (instanceState.isCoarsePointer
10526
10665
  ? this.options.coarseDragDistanceSquared
10527
10666
  : this.options.dragDistanceSquared) /
10528
10667
  cz
10529
10668
  ) {
10530
10669
  // Start dragging
10531
- inputs.isDragging = true
10670
+ inputs.setIsDragging(true)
10532
10671
  clearTimeout(this._longPressTimeout)
10533
10672
  }
10534
10673
  break
10535
10674
  }
10536
10675
  case 'pointer_up': {
10537
10676
  // Stop dragging / pointing
10538
- inputs.isDragging = false
10539
- inputs.isPointing = false
10677
+ inputs.setIsDragging(false)
10678
+ inputs.setIsPointing(false)
10540
10679
  clearTimeout(this._longPressTimeout)
10541
10680
 
10542
10681
  // Remove the button from the buttons set
@@ -10553,12 +10692,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10553
10692
  info.button = 0
10554
10693
  }
10555
10694
 
10556
- if (inputs.isPanning) {
10695
+ if (inputs.getIsPanning()) {
10557
10696
  if (!inputs.keys.has('Space')) {
10558
- inputs.isPanning = false
10559
- inputs.isSpacebarPanning = false
10697
+ inputs.setIsPanning(false)
10698
+ inputs.setIsSpacebarPanning(false)
10560
10699
  }
10561
- const slideDirection = this.inputs.pointerVelocity
10700
+ const slideDirection = this.inputs.getPointerVelocity()
10562
10701
  const slideSpeed = Math.min(2, slideDirection.len())
10563
10702
 
10564
10703
  switch (info.button) {
@@ -10602,43 +10741,48 @@ export class Editor extends EventEmitter<TLEventMap> {
10602
10741
  // Add the key from the keys set
10603
10742
  inputs.keys.add(info.code)
10604
10743
 
10605
- // If the space key is pressed (but meta / control isn't!) activate panning
10606
- if (info.code === 'Space' && !info.ctrlKey) {
10607
- if (!this.inputs.isPanning) {
10608
- this._prevCursor = instanceState.cursor.type
10609
- }
10744
+ if (this.options.spacebarPanning) {
10745
+ // If the space key is pressed (but meta / control isn't!) activate panning
10746
+ if (info.code === 'Space' && !info.ctrlKey) {
10747
+ if (!this.inputs.getIsPanning()) {
10748
+ this._prevCursor = instanceState.cursor.type
10749
+ }
10610
10750
 
10611
- this.inputs.isPanning = true
10612
- this.inputs.isSpacebarPanning = true
10613
- clearTimeout(this._longPressTimeout)
10614
- this.setCursor({ type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 })
10615
- }
10751
+ this.inputs.setIsPanning(true)
10752
+ this.inputs.setIsSpacebarPanning(true)
10753
+ clearTimeout(this._longPressTimeout)
10754
+ this.setCursor({
10755
+ type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
10756
+ rotation: 0,
10757
+ })
10758
+ }
10616
10759
 
10617
- if (this.inputs.isSpacebarPanning) {
10618
- let offset: Vec | undefined
10619
- switch (info.code) {
10620
- case 'ArrowUp': {
10621
- offset = new Vec(0, -1)
10622
- break
10623
- }
10624
- case 'ArrowRight': {
10625
- offset = new Vec(1, 0)
10626
- break
10627
- }
10628
- case 'ArrowDown': {
10629
- offset = new Vec(0, 1)
10630
- break
10631
- }
10632
- case 'ArrowLeft': {
10633
- offset = new Vec(-1, 0)
10634
- break
10760
+ if (this.inputs.getIsSpacebarPanning()) {
10761
+ let offset: Vec | undefined
10762
+ switch (info.code) {
10763
+ case 'ArrowUp': {
10764
+ offset = new Vec(0, -1)
10765
+ break
10766
+ }
10767
+ case 'ArrowRight': {
10768
+ offset = new Vec(1, 0)
10769
+ break
10770
+ }
10771
+ case 'ArrowDown': {
10772
+ offset = new Vec(0, 1)
10773
+ break
10774
+ }
10775
+ case 'ArrowLeft': {
10776
+ offset = new Vec(-1, 0)
10777
+ break
10778
+ }
10635
10779
  }
10636
- }
10637
10780
 
10638
- if (offset) {
10639
- const bounds = this.getViewportPageBounds()
10640
- const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10641
- this._animateToViewport(next, { animation: { duration: 320 } })
10781
+ if (offset) {
10782
+ const bounds = this.getViewportPageBounds()
10783
+ const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10784
+ this._animateToViewport(next, { animation: { duration: 320 } })
10785
+ }
10642
10786
  }
10643
10787
  }
10644
10788
 
@@ -10648,15 +10792,17 @@ export class Editor extends EventEmitter<TLEventMap> {
10648
10792
  // Remove the key from the keys set
10649
10793
  inputs.keys.delete(info.code)
10650
10794
 
10651
- // If we've lifted the space key,
10652
- if (info.code === 'Space') {
10653
- if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10654
- // If we're still middle dragging, continue panning
10655
- } else {
10656
- // otherwise, stop panning
10657
- this.inputs.isPanning = false
10658
- this.inputs.isSpacebarPanning = false
10659
- this.setCursor({ type: this._prevCursor, rotation: 0 })
10795
+ if (this.options.spacebarPanning) {
10796
+ // If we've lifted the space key,
10797
+ if (info.code === 'Space') {
10798
+ if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10799
+ // If we're still middle dragging, continue panning
10800
+ } else {
10801
+ // otherwise, stop panning
10802
+ this.inputs.setIsPanning(false)
10803
+ this.inputs.setIsSpacebarPanning(false)
10804
+ this.setCursor({ type: this._prevCursor, rotation: 0 })
10805
+ }
10660
10806
  }
10661
10807
  }
10662
10808
  break
@@ -10730,7 +10876,10 @@ function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) {
10730
10876
 
10731
10877
  function applyPartialToRecordWithProps<
10732
10878
  T extends UnknownRecord & { type: string; props: object; meta: object },
10733
- >(prev: T, partial?: Partial<T> & { props?: Partial<T['props']> }): T {
10879
+ >(
10880
+ prev: T,
10881
+ partial?: T extends T ? Omit<Partial<T>, 'props'> & { props?: Partial<T['props']> } : never
10882
+ ): T {
10734
10883
  if (!partial) return prev
10735
10884
  let next = null as null | T
10736
10885
  const entries = Object.entries(partial)