@tldraw/editor 4.3.0-next.f4772c19540d → 4.3.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 (195) hide show
  1. package/dist-cjs/index.d.ts +498 -155
  2. package/dist-cjs/index.js +6 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  5. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +3 -3
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/constants.js +1 -3
  10. package/dist-cjs/lib/constants.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +292 -286
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  14. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  15. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +18 -17
  16. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  17. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  18. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  22. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  24. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  25. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  26. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  28. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/BaseBoxShapeUtil.js.map +1 -1
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  32. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  33. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  34. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.js.map +2 -2
  36. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  37. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  38. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  39. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  40. package/dist-cjs/lib/exports/parseCss.js +1 -1
  41. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  42. package/dist-cjs/lib/globals/environment.js +45 -9
  43. package/dist-cjs/lib/globals/environment.js.map +2 -2
  44. package/dist-cjs/lib/globals/menus.js +1 -1
  45. package/dist-cjs/lib/globals/menus.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  47. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  48. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  49. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  50. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  52. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  53. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  54. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  55. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  56. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  57. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  58. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  59. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  60. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  61. package/dist-cjs/lib/options.js +6 -1
  62. package/dist-cjs/lib/options.js.map +2 -2
  63. package/dist-cjs/lib/primitives/Box.js +3 -0
  64. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  65. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  66. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  67. package/dist-cjs/lib/utils/reparenting.js.map +2 -2
  68. package/dist-cjs/lib/utils/rotation.js +1 -1
  69. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  70. package/dist-cjs/version.js +3 -3
  71. package/dist-cjs/version.js.map +1 -1
  72. package/dist-esm/index.d.mts +498 -155
  73. package/dist-esm/index.mjs +7 -2
  74. package/dist-esm/index.mjs.map +2 -2
  75. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  76. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  77. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  78. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +3 -3
  79. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  80. package/dist-esm/lib/constants.mjs +1 -3
  81. package/dist-esm/lib/constants.mjs.map +2 -2
  82. package/dist-esm/lib/editor/Editor.mjs +293 -289
  83. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  84. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  85. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  86. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +18 -17
  87. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  88. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  89. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  90. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  91. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  92. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  93. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  94. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  95. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  96. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  97. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  98. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  99. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  100. package/dist-esm/lib/editor/shapes/BaseBoxShapeUtil.mjs.map +1 -1
  101. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  102. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  103. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  104. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  105. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  106. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.mjs.map +2 -2
  107. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  108. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  109. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  110. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  111. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  112. package/dist-esm/lib/globals/environment.mjs +45 -9
  113. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  114. package/dist-esm/lib/globals/menus.mjs +1 -1
  115. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  116. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  117. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  118. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  119. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  120. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  121. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  122. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  123. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  124. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  125. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  126. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  127. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  128. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  129. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  130. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  131. package/dist-esm/lib/options.mjs +6 -1
  132. package/dist-esm/lib/options.mjs.map +2 -2
  133. package/dist-esm/lib/primitives/Box.mjs +3 -0
  134. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  135. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  136. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  137. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  138. package/dist-esm/lib/utils/rotation.mjs +1 -1
  139. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  140. package/dist-esm/version.mjs +3 -3
  141. package/dist-esm/version.mjs.map +1 -1
  142. package/editor.css +14 -12
  143. package/package.json +18 -16
  144. package/src/index.ts +4 -1
  145. package/src/lib/components/ErrorBoundary.tsx +1 -1
  146. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  147. package/src/lib/components/default-components/DefaultCanvas.tsx +4 -3
  148. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  149. package/src/lib/constants.ts +0 -2
  150. package/src/lib/editor/Editor.test.ts +150 -10
  151. package/src/lib/editor/Editor.ts +459 -379
  152. package/src/lib/editor/bindings/BindingUtil.ts +15 -9
  153. package/src/lib/editor/derivations/bindingsIndex.ts +2 -2
  154. package/src/lib/editor/derivations/notVisibleShapes.ts +37 -23
  155. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  156. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  157. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  158. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  159. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  160. package/src/lib/editor/managers/FontManager/FontManager.test.ts +14 -4
  161. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  162. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  163. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  164. package/src/lib/editor/managers/SnapManager/SnapManager.ts +4 -4
  165. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  166. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  167. package/src/lib/editor/shapes/BaseBoxShapeUtil.tsx +2 -2
  168. package/src/lib/editor/shapes/ShapeUtil.ts +72 -32
  169. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  170. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -3
  171. package/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts +2 -1
  172. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +6 -6
  173. package/src/lib/editor/types/emit-types.ts +3 -1
  174. package/src/lib/exports/getSvgJsx.test.ts +10 -19
  175. package/src/lib/exports/getSvgJsx.tsx +2 -5
  176. package/src/lib/exports/parseCss.test.ts +1 -0
  177. package/src/lib/exports/parseCss.ts +1 -1
  178. package/src/lib/globals/environment.ts +65 -10
  179. package/src/lib/globals/menus.ts +1 -1
  180. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  181. package/src/lib/hooks/useEvent.tsx +1 -1
  182. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  183. package/src/lib/hooks/useGestureEvents.ts +2 -2
  184. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  185. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  186. package/src/lib/hooks/useScreenBounds.ts +1 -1
  187. package/src/lib/hooks/useStateAttribute.ts +4 -1
  188. package/src/lib/hooks/useTransform.ts +1 -1
  189. package/src/lib/hooks/useZoomCss.ts +3 -8
  190. package/src/lib/options.ts +32 -0
  191. package/src/lib/primitives/Box.ts +9 -0
  192. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  193. package/src/lib/utils/reparenting.ts +5 -5
  194. package/src/lib/utils/rotation.ts +1 -1
  195. 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,18 @@ 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'
158
152
  import { TextManager } from './managers/TextManager/TextManager'
159
153
  import { TickManager } from './managers/TickManager/TickManager'
160
154
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
161
- import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
155
+ import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
162
156
  import { RootState } from './tools/RootState'
163
157
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
164
158
  import { TLContent } from './types/clipboard-types'
165
159
  import { TLEventMap } from './types/emit-types'
166
- import {
167
- TLEventInfo,
168
- TLPinchEventInfo,
169
- TLPointerEventInfo,
170
- TLWheelEventInfo,
171
- } from './types/event-types'
160
+ import { TLEventInfo, TLPointerEventInfo } from './types/event-types'
172
161
  import { TLExternalAsset, TLExternalContent } from './types/external-content'
173
162
  import { TLHistoryBatchOptions } from './types/history-types'
174
163
  import {
@@ -199,7 +188,7 @@ export type TLResizeShapeOptions = Partial<{
199
188
  /** @public */
200
189
  export interface TLEditorOptions {
201
190
  /**
202
- * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading
191
+ * The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading
203
192
  * from a server or database.
204
193
  */
205
194
  store: TLStore
@@ -337,6 +326,8 @@ export class Editor extends EventEmitter<TLEventMap> {
337
326
 
338
327
  this._tickManager = new TickManager(this)
339
328
 
329
+ this.inputs = new InputsManager(this)
330
+
340
331
  class NewRoot extends RootState {
341
332
  static override initial = initialState ?? ''
342
333
  }
@@ -447,7 +438,7 @@ export class Editor extends EventEmitter<TLEventMap> {
447
438
  let deletedBindings = new Map<TLBindingId, BindingOnDeleteOptions<any>>()
448
439
  const deletedShapeIds = new Set<TLShapeId>()
449
440
  const invalidParents = new Set<TLShapeId>()
450
- let invalidBindingTypes = new Set<string>()
441
+ let invalidBindingTypes = new Set<TLBinding['type']>()
451
442
  this.disposables.add(
452
443
  this.sideEffects.registerOperationCompleteHandler(() => {
453
444
  // this needs to be cleared here because further effects may delete more shapes
@@ -710,7 +701,7 @@ export class Editor extends EventEmitter<TLEventMap> {
710
701
  if (filtered.length > 0) {
711
702
  const commonGroupAncestor = this.findCommonAncestor(
712
703
  compact(filtered.map((id) => this.getShape(id))),
713
- (shape) => this.isShapeOfType<TLGroupShape>(shape, 'group')
704
+ (shape) => this.isShapeOfType(shape, 'group')
714
705
  )
715
706
 
716
707
  if (commonGroupAncestor) {
@@ -871,7 +862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
871
862
  }
872
863
 
873
864
  /**
874
- * A set of functions to call when the app is disposed.
865
+ * A set of functions to call when the editor is disposed.
875
866
  *
876
867
  * @public
877
868
  */
@@ -884,11 +875,21 @@ export class Editor extends EventEmitter<TLEventMap> {
884
875
  */
885
876
  isDisposed = false
886
877
 
887
- /** @internal */
888
- private readonly _tickManager
878
+ /**
879
+ * A manager for the editor's tick events.
880
+ *
881
+ * @internal */
882
+ private readonly _tickManager: TickManager
883
+
884
+ /**
885
+ * A manager for the editor's input state.
886
+ *
887
+ * @public
888
+ */
889
+ readonly inputs: InputsManager
889
890
 
890
891
  /**
891
- * A manager for the app's snapping feature.
892
+ * A manager for the editor's snapping feature.
892
893
  *
893
894
  * @public
894
895
  */
@@ -973,6 +974,7 @@ export class Editor extends EventEmitter<TLEventMap> {
973
974
  this.disposables.clear()
974
975
  this.store.dispose()
975
976
  this.isDisposed = true
977
+ this.emit('dispose')
976
978
  }
977
979
 
978
980
  /* ------------------- Shape Utils ------------------ */
@@ -982,7 +984,7 @@ export class Editor extends EventEmitter<TLEventMap> {
982
984
  *
983
985
  * @public
984
986
  */
985
- shapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }
987
+ shapeUtils: { readonly [K in string]?: ShapeUtil<TLShape> }
986
988
 
987
989
  styleProps: { [key: string]: Map<StyleProp<any>, string> }
988
990
 
@@ -1001,8 +1003,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1001
1003
  *
1002
1004
  * @public
1003
1005
  */
1004
- getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>
1005
- getShapeUtil<S extends TLUnknownShape>(type: S['type']): ShapeUtil<S>
1006
+ getShapeUtil<K extends TLShape['type']>(type: K): ShapeUtil<Extract<TLShape, { type: K }>>
1007
+ getShapeUtil<S extends TLShape>(shape: S | TLShapePartial<S> | S['type']): ShapeUtil<S>
1006
1008
  getShapeUtil<T extends ShapeUtil>(type: T extends ShapeUtil<infer R> ? R['type'] : string): T
1007
1009
  getShapeUtil(arg: string | { type: string }) {
1008
1010
  const type = typeof arg === 'string' ? arg : arg.type
@@ -1016,8 +1018,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1016
1018
  *
1017
1019
  * @param shape - A shape, shape partial, or shape type.
1018
1020
  */
1019
- hasShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): boolean
1020
- hasShapeUtil<S extends TLUnknownShape>(type: S['type']): boolean
1021
+ hasShapeUtil(shape: TLShape | TLShapePartial<TLShape>): boolean
1022
+ hasShapeUtil(type: TLShape['type']): boolean
1021
1023
  hasShapeUtil<T extends ShapeUtil>(
1022
1024
  type: T extends ShapeUtil<infer R> ? R['type'] : string
1023
1025
  ): boolean
@@ -1032,7 +1034,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1032
1034
  *
1033
1035
  * @public
1034
1036
  */
1035
- bindingUtils: { readonly [K in string]?: BindingUtil<TLUnknownBinding> }
1037
+ bindingUtils: { readonly [K in string]?: BindingUtil<TLBinding> }
1036
1038
 
1037
1039
  /**
1038
1040
  * Get a binding util from a binding itself.
@@ -1049,8 +1051,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1049
1051
  *
1050
1052
  * @public
1051
1053
  */
1052
- getBindingUtil<S extends TLUnknownBinding>(binding: S | { type: S['type'] }): BindingUtil<S>
1053
- getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>
1054
+ getBindingUtil<K extends TLBinding['type']>(type: K): BindingUtil<Extract<TLBinding, { type: K }>>
1055
+ getBindingUtil<S extends TLBinding>(binding: S | { type: S['type'] }): BindingUtil<S>
1054
1056
  getBindingUtil<T extends BindingUtil>(
1055
1057
  type: T extends BindingUtil<infer R> ? R['type'] : string
1056
1058
  ): T
@@ -1064,7 +1066,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1064
1066
  /* --------------------- History -------------------- */
1065
1067
 
1066
1068
  /**
1067
- * A manager for the app's history.
1069
+ * A manager for the editor's history.
1068
1070
  *
1069
1071
  * @readonly
1070
1072
  */
@@ -1088,14 +1090,18 @@ export class Editor extends EventEmitter<TLEventMap> {
1088
1090
  }
1089
1091
 
1090
1092
  /**
1091
- * Whether the app can undo.
1093
+ * Whether the editor can undo.
1092
1094
  *
1093
1095
  * @public
1094
1096
  */
1095
- @computed getCanUndo(): boolean {
1097
+ @computed canUndo(): boolean {
1096
1098
  return this.history.getNumUndos() > 0
1097
1099
  }
1098
1100
 
1101
+ getCanUndo() {
1102
+ return this.canUndo()
1103
+ }
1104
+
1099
1105
  /**
1100
1106
  * Redo to the next mark.
1101
1107
  *
@@ -1113,20 +1119,24 @@ export class Editor extends EventEmitter<TLEventMap> {
1113
1119
  return this
1114
1120
  }
1115
1121
 
1116
- clearHistory() {
1117
- this.history.clear()
1118
- return this
1119
- }
1120
-
1121
1122
  /**
1122
- * Whether the app can redo.
1123
+ * Whether the editor can redo.
1123
1124
  *
1124
1125
  * @public
1125
1126
  */
1126
- @computed getCanRedo(): boolean {
1127
+ @computed canRedo(): boolean {
1127
1128
  return this.history.getNumRedos() > 0
1128
1129
  }
1129
1130
 
1131
+ getCanRedo() {
1132
+ return this.canRedo()
1133
+ }
1134
+
1135
+ clearHistory() {
1136
+ this.history.clear()
1137
+ return this
1138
+ }
1139
+
1130
1140
  /**
1131
1141
  * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
1132
1142
  * any redos. You typically want to do this just before a user interaction begins or is handled.
@@ -1300,7 +1310,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1300
1310
  }),
1301
1311
  selectionCount: this.getSelectedShapes().length,
1302
1312
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1303
- inputs: this.inputs,
1313
+ inputs: this.inputs.toJson(),
1304
1314
  pageState: this.getCurrentPageState(),
1305
1315
  instanceState: this.getInstanceState(),
1306
1316
  collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
@@ -1325,7 +1335,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1325
1335
  * we're in a transaction that's about to be rolled back due to the same error we're currently
1326
1336
  * reporting.
1327
1337
  *
1328
- * Instead, to listen to changes to this value, you need to listen to app's `crash` event.
1338
+ * Instead, to listen to changes to this value, you need to listen to editor's `crash` event.
1329
1339
  *
1330
1340
  * @internal
1331
1341
  */
@@ -2028,7 +2038,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2028
2038
  }
2029
2039
 
2030
2040
  /**
2031
- * The id of the app's only selected shape.
2041
+ * The id of the editor's only selected shape.
2032
2042
  *
2033
2043
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
2034
2044
  *
@@ -2040,7 +2050,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2040
2050
  }
2041
2051
 
2042
2052
  /**
2043
- * The app's only selected shape.
2053
+ * The editor's only selected shape.
2044
2054
  *
2045
2055
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
2046
2056
  *
@@ -2220,7 +2230,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2220
2230
  throw Error(`Editor.setFocusedGroup: Shape with id ${id} does not exist`)
2221
2231
  }
2222
2232
 
2223
- if (!this.isShapeOfType<TLGroupShape>(shape, 'group')) {
2233
+ if (!this.isShapeOfType(shape, 'group')) {
2224
2234
  throw Error(
2225
2235
  `Editor.setFocusedGroup: Cannot set focused group to shape of type ${shape.type}`
2226
2236
  )
@@ -2248,7 +2258,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2248
2258
  if (focusedGroup) {
2249
2259
  // If we have a focused layer, look for an ancestor of the focused shape that is a group
2250
2260
  const match = this.findShapeAncestor(focusedGroup, (shape) =>
2251
- this.isShapeOfType<TLGroupShape>(shape, 'group')
2261
+ this.isShapeOfType(shape, 'group')
2252
2262
  )
2253
2263
  // If we have an ancestor that can become a focused layer, set it as the focused layer
2254
2264
  this.setFocusedGroup(match?.id ?? null)
@@ -2281,6 +2291,29 @@ export class Editor extends EventEmitter<TLEventMap> {
2281
2291
  return editingShapeId ? this.getShape(editingShapeId) : undefined
2282
2292
  }
2283
2293
 
2294
+ /**
2295
+ * Whether the shape can be edited.
2296
+ *
2297
+ * @param shape - The shape (or shape id) to check if it can be edited.
2298
+ * @param info - The info about the edit start.
2299
+ *
2300
+ * @public
2301
+ * @returns true if the shape can be edited, false otherwise.
2302
+ */
2303
+ canEditShape<T extends TLShape | TLShapeId>(shape: T | null, info?: TLEditStartInfo): shape is T {
2304
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2305
+ if (!id) return false // no shape
2306
+ if (id === this.getEditingShapeId()) return false // already editing this shape
2307
+ const _shape = this.getShape(id)
2308
+ if (!_shape) return false // no shape
2309
+ const util = this.getShapeUtil(_shape)
2310
+ const _info: TLEditStartInfo = info ?? { type: 'unknown' }
2311
+ if (!util.canEdit(_shape, _info)) return false // shape is not editable
2312
+ if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception
2313
+ 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.
2314
+ return true // shape is editable
2315
+ }
2316
+
2284
2317
  /**
2285
2318
  * Set the current editing shape.
2286
2319
  *
@@ -2296,44 +2329,59 @@ export class Editor extends EventEmitter<TLEventMap> {
2296
2329
  */
2297
2330
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2298
2331
  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
2332
 
2322
- // Either we just set the editing id to null, or the shape was missing or not editable
2333
+ if (!id) {
2334
+ // setting the editing shape to null
2323
2335
  this.run(
2324
2336
  () => {
2325
- this._updateCurrentPageState({ editingShapeId: null })
2326
- this._currentRichTextEditor.set(null)
2337
+ // Clean up the previous editing shape
2338
+ const prevEditingShapeId = this.getEditingShapeId()
2327
2339
  if (prevEditingShapeId) {
2328
2340
  const prevEditingShape = this.getShape(prevEditingShapeId)
2329
2341
  if (prevEditingShape) {
2330
2342
  this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2331
2343
  }
2332
2344
  }
2345
+
2346
+ // Clean up the editing shape state and rich text editor
2347
+ this._updateCurrentPageState({ editingShapeId: null })
2348
+ this._currentRichTextEditor.set(null)
2333
2349
  },
2334
2350
  { history: 'ignore' }
2335
2351
  )
2352
+
2353
+ return this
2336
2354
  }
2355
+
2356
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2357
+ if (!this.canEditShape(id)) return this
2358
+
2359
+ // id was provided and the next editing shape is editable, so set the rich text editor to null
2360
+ this.run(
2361
+ () => {
2362
+ // Clean up the previous editing shape
2363
+ const prevEditingShapeId = this.getEditingShapeId()
2364
+ if (prevEditingShapeId) {
2365
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2366
+ if (prevEditingShape) {
2367
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2368
+ }
2369
+ }
2370
+
2371
+ // Clean up the editing shape state and rich text editor
2372
+ this._updateCurrentPageState({ editingShapeId: null })
2373
+ this._currentRichTextEditor.set(null)
2374
+
2375
+ // Set the new editing shape
2376
+ this.select(id)
2377
+ this._updateCurrentPageState({ editingShapeId: id })
2378
+
2379
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2380
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2381
+ },
2382
+ { history: 'ignore' }
2383
+ )
2384
+
2337
2385
  return this
2338
2386
  }
2339
2387
 
@@ -2537,6 +2585,26 @@ export class Editor extends EventEmitter<TLEventMap> {
2537
2585
  return this.getCurrentPageState().croppingShapeId
2538
2586
  }
2539
2587
 
2588
+ /**
2589
+ * Whether the shape can be cropped.
2590
+ *
2591
+ * @param shape - The shape (or shape id) to check if it can be cropped.
2592
+ *
2593
+ * @public
2594
+ * @returns true if the shape can be cropped, false otherwise.
2595
+ */
2596
+ canCropShape<T extends TLShape | TLShapeId>(shape: T | null): shape is T {
2597
+ if (!shape) return false
2598
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2599
+ if (!id) return false
2600
+ const _shape = this.getShape(id)
2601
+ if (!_shape) return false
2602
+ const util = this.getShapeUtil(_shape)
2603
+ if (!util.canCrop(_shape)) return false
2604
+ if (this.isShapeOrAncestorLocked(_shape)) return false
2605
+ return true
2606
+ }
2607
+
2540
2608
  /**
2541
2609
  * Set the current cropping shape.
2542
2610
  *
@@ -2558,12 +2626,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2558
2626
  () => {
2559
2627
  if (!id) {
2560
2628
  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
- }
2629
+ } else if (this.canCropShape(id)) {
2630
+ this.updateCurrentPageState({ croppingShapeId: id })
2567
2631
  }
2568
2632
  },
2569
2633
  { history: 'ignore' }
@@ -2673,6 +2737,52 @@ export class Editor extends EventEmitter<TLEventMap> {
2673
2737
  return this.getCamera().z
2674
2738
  }
2675
2739
 
2740
+ private _debouncedZoomLevel = atom('debounced zoom level', 1)
2741
+
2742
+ /**
2743
+ * Get the debounced zoom level. When the camera is moving, this returns the zoom level
2744
+ * from when the camera started moving rather than the current zoom level. This can be
2745
+ * used to avoid expensive re-renders during camera movements.
2746
+ *
2747
+ * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom`
2748
+ * is `false`, this method always returns the current zoom level.
2749
+ *
2750
+ * @public
2751
+ */
2752
+ @computed getDebouncedZoomLevel() {
2753
+ if (this.options.debouncedZoom) {
2754
+ if (this.getCameraState() === 'idle') {
2755
+ return this.getZoomLevel()
2756
+ } else {
2757
+ return this._debouncedZoomLevel.get()
2758
+ }
2759
+ }
2760
+
2761
+ return this.getZoomLevel()
2762
+ }
2763
+
2764
+ @computed private _getAboveDebouncedZoomThreshold() {
2765
+ return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold
2766
+ }
2767
+
2768
+ /**
2769
+ * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2770
+ * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2771
+ *
2772
+ * @public
2773
+ * @example
2774
+ * ```ts
2775
+ * editor.getEfficientZoomLevel()
2776
+ * ```
2777
+ *
2778
+ * @public
2779
+ */
2780
+ @computed getEfficientZoomLevel() {
2781
+ return this._getAboveDebouncedZoomThreshold()
2782
+ ? this.getDebouncedZoomLevel()
2783
+ : this.getZoomLevel()
2784
+ }
2785
+
2676
2786
  /**
2677
2787
  * Get the camera's initial or reset zoom level.
2678
2788
  *
@@ -2999,7 +3109,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2999
3109
 
3000
3110
  // Dispatch a new pointer move because the pointer's page will have changed
3001
3111
  // (its screen position will compute to a new page position given the new camera position)
3002
- const { currentScreenPoint, currentPagePoint } = this.inputs
3112
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
3113
+ const currentPagePoint = this.inputs.getCurrentPagePoint()
3003
3114
 
3004
3115
  // compare the next page point (derived from the current camera) to the current page point
3005
3116
  if (
@@ -3163,7 +3274,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3163
3274
  * ```ts
3164
3275
  * editor.zoomIn()
3165
3276
  * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
3166
- * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
3277
+ * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
3167
3278
  * ```
3168
3279
  *
3169
3280
  * @param point - The screen point to zoom in on. Defaults to the screen center
@@ -3208,7 +3319,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3208
3319
  * ```ts
3209
3320
  * editor.zoomOut()
3210
3321
  * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
3211
- * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
3322
+ * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } })
3212
3323
  * ```
3213
3324
  *
3214
3325
  * @param point - The point to zoom out on. Defaults to the viewport screen center.
@@ -3265,10 +3376,17 @@ export class Editor extends EventEmitter<TLEventMap> {
3265
3376
 
3266
3377
  const selectionPageBounds = this.getSelectionPageBounds()
3267
3378
  if (selectionPageBounds) {
3268
- this.zoomToBounds(selectionPageBounds, {
3269
- targetZoom: Math.max(1, this.getZoomLevel()),
3270
- ...opts,
3271
- })
3379
+ const currentZoom = this.getZoomLevel()
3380
+ // If already at 100%, zoom to fit the selection in the viewport
3381
+ // Otherwise, zoom to 100% centered on the selection
3382
+ if (Math.abs(currentZoom - 1) < 0.01) {
3383
+ this.zoomToBounds(selectionPageBounds, opts)
3384
+ } else {
3385
+ this.zoomToBounds(selectionPageBounds, {
3386
+ targetZoom: 1,
3387
+ ...opts,
3388
+ })
3389
+ }
3272
3390
  }
3273
3391
  return this
3274
3392
  }
@@ -3325,7 +3443,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3325
3443
 
3326
3444
  const viewportScreenBounds = this.getViewportScreenBounds()
3327
3445
 
3328
- const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28)
3446
+ const inset =
3447
+ opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28)
3329
3448
 
3330
3449
  const baseZoom = this.getBaseZoom()
3331
3450
  const zoomMin = cameraOptions.zoomSteps[0]
@@ -3635,22 +3754,23 @@ export class Editor extends EventEmitter<TLEventMap> {
3635
3754
  if (_willSetInitialBounds) {
3636
3755
  // If we have just received the initial bounds, don't center the camera.
3637
3756
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3757
+ this.emit('resize', screenBounds.toJson())
3638
3758
  this.setCamera(this.getCamera())
3639
3759
  } else {
3640
3760
  if (center && !this.getInstanceState().followingUserId) {
3641
3761
  // Get the page center before the change, make the change, and restore it
3642
3762
  const before = this.getViewportPageBounds().center
3643
3763
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3764
+ this.emit('resize', screenBounds.toJson())
3644
3765
  this.centerOnPoint(before)
3645
3766
  } else {
3646
3767
  // Otherwise,
3647
3768
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3769
+ this.emit('resize', screenBounds.toJson())
3648
3770
  this._setCamera(Vec.From({ ...this.getCamera() }))
3649
3771
  }
3650
3772
  }
3651
3773
 
3652
- this._tickCameraState()
3653
-
3654
3774
  return this
3655
3775
  }
3656
3776
 
@@ -4056,18 +4176,19 @@ export class Editor extends EventEmitter<TLEventMap> {
4056
4176
  // box just for rendering, and we only update after the camera stops moving.
4057
4177
  private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4058
4178
  private _cameraStateTimeoutRemaining = 0
4059
- _decayCameraStateTimeout(elapsed: number) {
4179
+ private _decayCameraStateTimeout(elapsed: number) {
4060
4180
  this._cameraStateTimeoutRemaining -= elapsed
4061
4181
  if (this._cameraStateTimeoutRemaining > 0) return
4062
4182
  this.off('tick', this._decayCameraStateTimeout)
4063
4183
  this._cameraState.set('idle')
4064
4184
  }
4065
- _tickCameraState() {
4185
+ private _tickCameraState() {
4066
4186
  // always reset the timeout
4067
4187
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4068
4188
  // If the state is idle, then start the tick
4069
4189
  if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4070
4190
  this._cameraState.set('moving')
4191
+ this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4071
4192
  this.on('tick', this._decayCameraStateTimeout)
4072
4193
  }
4073
4194
 
@@ -5127,10 +5248,10 @@ export class Editor extends EventEmitter<TLEventMap> {
5127
5248
 
5128
5249
  // Check labels first
5129
5250
  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')) &&
5251
+ this.isShapeOfType(shape, 'frame') ||
5252
+ ((this.isShapeOfType(shape, 'note') ||
5253
+ this.isShapeOfType(shape, 'arrow') ||
5254
+ (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) &&
5134
5255
  this.getShapeUtil(shape).getText(shape)?.trim())
5135
5256
  ) {
5136
5257
  for (const childGeometry of (geometry as Group2d).children) {
@@ -5140,7 +5261,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5140
5261
  }
5141
5262
  }
5142
5263
 
5143
- if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5264
+ if (this.isShapeOfType(shape, 'frame')) {
5144
5265
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5145
5266
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5146
5267
 
@@ -5421,7 +5542,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5421
5542
  *
5422
5543
  * @example
5423
5544
  * ```ts
5424
- * const isArrowShape = isShapeOfType<TLArrowShape>(someShape, 'arrow')
5545
+ * const isArrowShape = isShapeOfType(someShape, 'arrow')
5425
5546
  * ```
5426
5547
  *
5427
5548
  * @param util - the TLShapeUtil constructor to test against
@@ -5429,15 +5550,16 @@ export class Editor extends EventEmitter<TLEventMap> {
5429
5550
  *
5430
5551
  * @public
5431
5552
  */
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'],
5553
+ isShapeOfType<K extends TLShape['type']>(
5554
+ shape: TLShape,
5555
+ type: K
5556
+ ): shape is Extract<TLShape, { type: K }>
5557
+ isShapeOfType<T extends TLShape>(
5558
+ shape: TLShape,
5439
5559
  type: T['type']
5440
- ) {
5560
+ ): shape is Extract<TLShape, { type: T['type'] }>
5561
+ isShapeOfType<T extends TLShape = TLShape>(shapeId: TLShapeId, type: T['type']): boolean
5562
+ isShapeOfType(arg: TLShape | TLShapeId, type: TLShape['type']) {
5441
5563
  const shape = typeof arg === 'string' ? this.getShape(arg) : arg
5442
5564
  if (!shape) return false
5443
5565
  return shape.type === type
@@ -5833,7 +5955,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5833
5955
 
5834
5956
  while (node) {
5835
5957
  if (
5836
- this.isShapeOfType<TLGroupShape>(node, 'group') &&
5958
+ this.isShapeOfType(node, 'group') &&
5837
5959
  focusedGroup?.id !== node.id &&
5838
5960
  !this.hasAncestor(focusedGroup, node.id) &&
5839
5961
  (filter?.(node) ?? true)
@@ -5875,7 +5997,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5875
5997
  * Get all bindings of a certain type _from_ a particular shape. These are the bindings whose
5876
5998
  * `fromId` matched the shape's ID.
5877
5999
  */
5878
- getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(
6000
+ getBindingsFromShape<K extends TLBinding['type']>(
6001
+ shape: TLShape | TLShapeId,
6002
+ type: K
6003
+ ): Extract<TLBinding, { type: K }>[]
6004
+ getBindingsFromShape<Binding extends TLBinding = TLBinding>(
6005
+ shape: TLShape | TLShapeId,
6006
+ type: Binding['type']
6007
+ ): Binding[]
6008
+ getBindingsFromShape<Binding extends TLBinding = TLBinding>(
5879
6009
  shape: TLShape | TLShapeId,
5880
6010
  type: Binding['type']
5881
6011
  ): Binding[] {
@@ -5889,7 +6019,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5889
6019
  * Get all bindings of a certain type _to_ a particular shape. These are the bindings whose
5890
6020
  * `toId` matches the shape's ID.
5891
6021
  */
5892
- getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(
6022
+ getBindingsToShape<K extends TLBinding['type']>(
6023
+ shape: TLShape | TLShapeId,
6024
+ type: K
6025
+ ): Extract<TLBinding, { type: K }>[]
6026
+ getBindingsToShape<Binding extends TLBinding = TLBinding>(
6027
+ shape: TLShape | TLShapeId,
6028
+ type: Binding['type']
6029
+ ): Binding[]
6030
+ getBindingsToShape<Binding extends TLBinding = TLBinding>(
5893
6031
  shape: TLShape | TLShapeId,
5894
6032
  type: Binding['type']
5895
6033
  ): Binding[] {
@@ -5903,7 +6041,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5903
6041
  * Get all bindings involving a particular shape. This includes bindings where the shape is the
5904
6042
  * `fromId` or `toId`. If a type is provided, only bindings of that type are returned.
5905
6043
  */
5906
- getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(
6044
+ getBindingsInvolvingShape<K extends TLBinding['type']>(
6045
+ shape: TLShape | TLShapeId,
6046
+ type: K
6047
+ ): Extract<TLBinding, { type: K }>[]
6048
+ getBindingsInvolvingShape<Binding extends TLBinding = TLBinding>(
6049
+ shape: TLShape | TLShapeId,
6050
+ type?: Binding['type']
6051
+ ): Binding[]
6052
+ getBindingsInvolvingShape<Binding extends TLBinding = TLBinding>(
5907
6053
  shape: TLShape | TLShapeId,
5908
6054
  type?: Binding['type']
5909
6055
  ): Binding[] {
@@ -5925,7 +6071,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5925
6071
  if (!fromShape || !toShape) continue
5926
6072
  if (!this.canBindShapes({ fromShape, toShape, binding: partial })) continue
5927
6073
 
5928
- const util = this.getBindingUtil<TLUnknownBinding>(partial.type)
6074
+ const util = this.getBindingUtil(partial.type)
5929
6075
  const defaultProps = util.getDefaultProps()
5930
6076
  const binding = this.store.schema.types.binding.create({
5931
6077
  ...partial,
@@ -6030,7 +6176,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6030
6176
  const toShapeType = typeof toShape === 'string' ? toShape : toShape.type
6031
6177
  const bindingType = typeof binding === 'string' ? binding : binding.type
6032
6178
 
6033
- const canBindOpts = { fromShapeType, toShapeType, bindingType }
6179
+ const canBindOpts = { fromShapeType, toShapeType, bindingType } as const
6034
6180
 
6035
6181
  if (fromShapeType === toShapeType) {
6036
6182
  return this.getShapeUtil(fromShapeType).canBind(canBindOpts)
@@ -6571,7 +6717,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6571
6717
  const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
6572
6718
 
6573
6719
  for (const shape of shapesToFlipFirstPass) {
6574
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
6720
+ if (this.isShapeOfType(shape, 'group')) {
6575
6721
  const childrenOfGroups = compact(
6576
6722
  this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
6577
6723
  )
@@ -7628,8 +7774,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7628
7774
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
7629
7775
  // to make sure the shape is mirrored correctly
7630
7776
  if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
7631
- let { rotation } = Mat.Decompose(options.initialPageTransform)
7632
- rotation -= 2 * rotation
7777
+ // We need to compute the new local rotation that will result in the negated page rotation.
7778
+ // For a shape with local rotation `localRot` and parent page rotation `parentRot`:
7779
+ // - pageRot = parentRot + localRot
7780
+ // - newPageRot = -pageRot (we want to negate the page rotation)
7781
+ // - newPageRot = parentRot + newLocalRot (parent hasn't changed)
7782
+ // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot
7783
+ const parentRotation = this.getShapeParentTransform(id).rotation()
7784
+ const rotation = -options.initialShape.rotation - 2 * parentRotation
7633
7785
  this.updateShapes([{ id, type, rotation }])
7634
7786
  }
7635
7787
 
@@ -7649,9 +7801,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7649
7801
  )
7650
7802
 
7651
7803
  // now calculate how far away the shape is from where it needs to be
7652
- const pageBounds = this.getShapePageBounds(id)!
7653
7804
  const pageTransform = this.getShapePageTransform(id)!
7654
- const currentPageCenter = pageBounds.center
7805
+ // We need to use the local bounds center transformed to page space, not the axis-aligned
7806
+ // page bounds center. This is because the page bounds are axis-aligned and their center
7807
+ // changes when the rotation changes, but we want to use the same reference point as
7808
+ // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform).
7809
+ const currentLocalBounds = this.getShapeGeometry(id).bounds
7810
+ const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
7655
7811
  const shapePageTransformOrigin = pageTransform.point()
7656
7812
  if (!currentPageCenter || !shapePageTransformOrigin) return this
7657
7813
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
@@ -7692,9 +7848,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7692
7848
  *
7693
7849
  * @public
7694
7850
  */
7695
- canCreateShape<T extends TLUnknownShape>(
7696
- shape: OptionalKeys<TLShapePartial<T>, 'id'> | T['id']
7697
- ): boolean {
7851
+ canCreateShape(shape: OptionalKeys<TLShapePartial<TLShape>, 'id'> | TLShape['id']): boolean {
7698
7852
  return this.canCreateShapes([shape])
7699
7853
  }
7700
7854
 
@@ -7705,8 +7859,8 @@ export class Editor extends EventEmitter<TLEventMap> {
7705
7859
  *
7706
7860
  * @public
7707
7861
  */
7708
- canCreateShapes<T extends TLUnknownShape>(
7709
- shapes: (T['id'] | OptionalKeys<TLShapePartial<T>, 'id'>)[]
7862
+ canCreateShapes(
7863
+ shapes: (TLShape['id'] | OptionalKeys<TLShapePartial<TLShape>, 'id'>)[]
7710
7864
  ): boolean {
7711
7865
  return shapes.length + this.getCurrentPageShapeIds().size <= this.options.maxShapesPerPage
7712
7866
  }
@@ -7724,7 +7878,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7724
7878
  *
7725
7879
  * @public
7726
7880
  */
7727
- createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this {
7881
+ createShape<TShape extends TLShape>(shape: TLCreateShapePartial<TShape>): this {
7728
7882
  this.createShapes([shape])
7729
7883
  return this
7730
7884
  }
@@ -7742,7 +7896,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7742
7896
  *
7743
7897
  * @public
7744
7898
  */
7745
- createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this {
7899
+ createShapes<TShape extends TLShape = TLShape>(shapes: TLCreateShapePartial<TShape>[]): this {
7746
7900
  if (!Array.isArray(shapes)) {
7747
7901
  throw Error('Editor.createShapes: must provide an array of shapes or shape partials')
7748
7902
  }
@@ -8101,7 +8255,12 @@ export class Editor extends EventEmitter<TLEventMap> {
8101
8255
  )
8102
8256
  )
8103
8257
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
8104
- const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
8258
+ const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
8259
+ const pageBounds = Box.Common(childBounds)
8260
+
8261
+ if (!pageBounds.isValid()) {
8262
+ throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`)
8263
+ }
8105
8264
 
8106
8265
  const { x, y } = pageBounds.point
8107
8266
 
@@ -8123,7 +8282,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8123
8282
  const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index
8124
8283
 
8125
8284
  this.run(() => {
8126
- this.createShapes<TLGroupShape>([
8285
+ this.createShapes([
8127
8286
  {
8128
8287
  id: groupId,
8129
8288
  type: 'group',
@@ -8193,7 +8352,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8193
8352
  const groups: TLGroupShape[] = []
8194
8353
 
8195
8354
  shapesToUngroup.forEach((shape) => {
8196
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8355
+ if (this.isShapeOfType(shape, 'group')) {
8197
8356
  groups.push(shape)
8198
8357
  } else {
8199
8358
  idsToSelect.add(shape.id)
@@ -8239,7 +8398,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8239
8398
  *
8240
8399
  * @public
8241
8400
  */
8242
- updateShape<T extends TLUnknownShape>(partial: TLShapePartial<T> | null | undefined) {
8401
+ updateShape<T extends TLShape = TLShape>(partial: TLShapePartial<T> | null | undefined) {
8243
8402
  this.updateShapes([partial])
8244
8403
  return this
8245
8404
  }
@@ -8256,7 +8415,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8256
8415
  *
8257
8416
  * @public
8258
8417
  */
8259
- updateShapes<T extends TLUnknownShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
8418
+ updateShapes<T extends TLShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
8260
8419
  const compactedPartials: TLShapePartial<T>[] = Array(partials.length)
8261
8420
 
8262
8421
  for (let i = 0, n = partials.length; i < n; i++) {
@@ -8408,7 +8567,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8408
8567
  * @internal
8409
8568
  */
8410
8569
  private _extractSharedStyles(shape: TLShape, sharedStyleMap: SharedStyleMap) {
8411
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8570
+ if (this.isShapeOfType(shape, 'group')) {
8412
8571
  // For groups, ignore the styles of the group shape and instead include the styles of the
8413
8572
  // group's children. These are the shapes that would have their styles changed if the
8414
8573
  // user called `setStyle` on the current selection.
@@ -8528,7 +8687,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8528
8687
  // For groups, ignore the opacity of the group shape and instead include
8529
8688
  // the opacity of the group's children. These are the shapes that would have
8530
8689
  // their opacity changed if the user called `setOpacity` on the current selection.
8531
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8690
+ if (this.isShapeOfType(shape, 'group')) {
8532
8691
  for (const childId of this.getSortedChildIdsForParent(shape.id)) {
8533
8692
  addShape(childId)
8534
8693
  }
@@ -8589,7 +8748,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8589
8748
  // We can have many deep levels of grouped shape
8590
8749
  // Making a recursive function to look through all the levels
8591
8750
  const addShapeById = (shape: TLShape) => {
8592
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8751
+ if (this.isShapeOfType(shape, 'group')) {
8593
8752
  const childIds = this.getSortedChildIdsForParent(shape)
8594
8753
  for (const childId of childIds) {
8595
8754
  addShapeById(this.getShape(childId)!)
@@ -8673,7 +8832,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8673
8832
  // We can have many deep levels of grouped shape
8674
8833
  // Making a recursive function to look through all the levels
8675
8834
  const addShapeById = (shape: TLShape) => {
8676
- if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
8835
+ if (this.isShapeOfType(shape, 'group')) {
8677
8836
  const childIds = this.getSortedChildIdsForParent(shape.id)
8678
8837
  for (const childId of childIds) {
8679
8838
  addShapeById(this.getShape(childId)!)
@@ -9098,7 +9257,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9098
9257
  for (const shape of this.getSelectedShapes()) {
9099
9258
  if (lowestDepth === 0) break
9100
9259
 
9101
- const isFrame = this.isShapeOfType<TLFrameShape>(shape, 'frame')
9260
+ const isFrame = this.isShapeOfType(shape, 'frame')
9102
9261
  const ancestors = this.getShapeAncestors(shape)
9103
9262
  if (isFrame) ancestors.push(shape)
9104
9263
 
@@ -9126,6 +9285,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9126
9285
  }
9127
9286
  }
9128
9287
 
9288
+ if (point) {
9289
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9290
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9291
+ if (rootShapesFromContent.length > 0) {
9292
+ const targetParent = this.getShapeAtPoint(point, {
9293
+ hitInside: true,
9294
+ hitFrameInside: true,
9295
+ hitLocked: true,
9296
+ filter: (shape) => {
9297
+ const util = this.getShapeUtil(shape)
9298
+ if (!util.canReceiveNewChildrenOfType) return false
9299
+ return rootShapesFromContent.every((rootShape) =>
9300
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9301
+ )
9302
+ },
9303
+ })
9304
+
9305
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9306
+ // parent under the pointer so that we don't keep using the original
9307
+ // selection's parent (which can keep shapes clipped inside frames).
9308
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9309
+ }
9310
+ }
9311
+
9129
9312
  let isDuplicating = false
9130
9313
 
9131
9314
  if (!isPageId(pasteParentId)) {
@@ -9137,8 +9320,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9137
9320
  if (rootShapeIds.length === 1) {
9138
9321
  const rootShape = shapes.find((s) => s.id === rootShapeIds[0])!
9139
9322
  if (
9140
- this.isShapeOfType<TLFrameShape>(parent, 'frame') &&
9141
- this.isShapeOfType<TLFrameShape>(rootShape, 'frame') &&
9323
+ this.isShapeOfType(parent, 'frame') &&
9324
+ this.isShapeOfType(rootShape, 'frame') &&
9142
9325
  rootShape.props.w === parent?.props.w &&
9143
9326
  rootShape.props.h === parent?.props.h
9144
9327
  ) {
@@ -9313,11 +9496,11 @@ export class Editor extends EventEmitter<TLEventMap> {
9313
9496
  const onlyRoot = rootShapes[0] as TLFrameShape
9314
9497
  // If the old bounds are in the viewport...
9315
9498
  // todo: replace frame references with shapes that can accept children
9316
- if (this.isShapeOfType<TLFrameShape>(onlyRoot, 'frame')) {
9499
+ if (this.isShapeOfType(onlyRoot, 'frame')) {
9317
9500
  while (
9318
9501
  this.getShapesAtPoint(point).some(
9319
9502
  (shape) =>
9320
- this.isShapeOfType<TLFrameShape>(shape, 'frame') &&
9503
+ this.isShapeOfType(shape, 'frame') &&
9321
9504
  shape.props.w === onlyRoot.props.w &&
9322
9505
  shape.props.h === onlyRoot.props.h
9323
9506
  )
@@ -9463,126 +9646,6 @@ export class Editor extends EventEmitter<TLEventMap> {
9463
9646
 
9464
9647
  /* --------------------- Events --------------------- */
9465
9648
 
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
9649
  /**
9587
9650
  * Dispatch a cancel event.
9588
9651
  *
@@ -9652,19 +9715,22 @@ export class Editor extends EventEmitter<TLEventMap> {
9652
9715
  // weird but true: what `inputs` calls screen-space is actually viewport space. so
9653
9716
  // we need to convert back into true screen space first. we should fix this...
9654
9717
  Vec.Add(
9655
- this.inputs.currentScreenPoint,
9718
+ this.inputs.getCurrentScreenPoint(),
9656
9719
  this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9657
9720
  ),
9658
9721
  pointerId: options?.pointerId ?? 0,
9659
9722
  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),
9723
+ isPen: options?.isPen ?? this.inputs.getIsPen(),
9724
+ shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(),
9725
+ altKey: options?.altKey ?? this.inputs.getAltKey(),
9726
+ ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(),
9727
+ metaKey: options?.metaKey ?? this.inputs.getMetaKey(),
9728
+ accelKey: false,
9666
9729
  }
9667
9730
 
9731
+ // needs to be calculated second
9732
+ event.accelKey = options?.accelKey ?? this.inputs.getAccelKey()
9733
+
9668
9734
  if (options?.immediate) {
9669
9735
  this._flushEventForTick(event)
9670
9736
  } else {
@@ -10037,16 +10103,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10037
10103
  /** @internal */
10038
10104
  @bind
10039
10105
  _setShiftKeyTimeout() {
10040
- this.inputs.shiftKey = false
10106
+ this.inputs.setShiftKey(false)
10041
10107
  this.dispatch({
10042
10108
  type: 'keyboard',
10043
10109
  name: 'key_up',
10044
10110
  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),
10111
+ shiftKey: this.inputs.getShiftKey(),
10112
+ ctrlKey: this.inputs.getCtrlKey(),
10113
+ altKey: this.inputs.getAltKey(),
10114
+ metaKey: this.inputs.getMetaKey(),
10115
+ accelKey: this.inputs.getAccelKey(),
10050
10116
  code: 'ShiftLeft',
10051
10117
  })
10052
10118
  }
@@ -10057,16 +10123,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10057
10123
  /** @internal */
10058
10124
  @bind
10059
10125
  _setAltKeyTimeout() {
10060
- this.inputs.altKey = false
10126
+ this.inputs.setAltKey(false)
10061
10127
  this.dispatch({
10062
10128
  type: 'keyboard',
10063
10129
  name: 'key_up',
10064
10130
  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),
10131
+ shiftKey: this.inputs.getShiftKey(),
10132
+ ctrlKey: this.inputs.getCtrlKey(),
10133
+ altKey: this.inputs.getAltKey(),
10134
+ metaKey: this.inputs.getMetaKey(),
10135
+ accelKey: this.inputs.getAccelKey(),
10070
10136
  code: 'AltLeft',
10071
10137
  })
10072
10138
  }
@@ -10077,16 +10143,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10077
10143
  /** @internal */
10078
10144
  @bind
10079
10145
  _setCtrlKeyTimeout() {
10080
- this.inputs.ctrlKey = false
10146
+ this.inputs.setCtrlKey(false)
10081
10147
  this.dispatch({
10082
10148
  type: 'keyboard',
10083
10149
  name: 'key_up',
10084
10150
  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),
10151
+ shiftKey: this.inputs.getShiftKey(),
10152
+ ctrlKey: this.inputs.getCtrlKey(),
10153
+ altKey: this.inputs.getAltKey(),
10154
+ metaKey: this.inputs.getMetaKey(),
10155
+ accelKey: this.inputs.getAccelKey(),
10090
10156
  code: 'ControlLeft',
10091
10157
  })
10092
10158
  }
@@ -10097,16 +10163,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10097
10163
  /** @internal */
10098
10164
  @bind
10099
10165
  _setMetaKeyTimeout() {
10100
- this.inputs.metaKey = false
10166
+ this.inputs.setMetaKey(false)
10101
10167
  this.dispatch({
10102
10168
  type: 'keyboard',
10103
10169
  name: 'key_up',
10104
10170
  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),
10171
+ shiftKey: this.inputs.getShiftKey(),
10172
+ ctrlKey: this.inputs.getCtrlKey(),
10173
+ altKey: this.inputs.getAltKey(),
10174
+ metaKey: this.inputs.getMetaKey(),
10175
+ accelKey: this.inputs.getAccelKey(),
10110
10176
  code: 'MetaLeft',
10111
10177
  })
10112
10178
  }
@@ -10114,9 +10180,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10114
10180
  /** @internal */
10115
10181
  private _restoreToolId = 'select'
10116
10182
 
10117
- /** @internal */
10118
- private _pinchStart = 1
10119
-
10120
10183
  /** @internal */
10121
10184
  private _didPinch = false
10122
10185
 
@@ -10223,55 +10286,54 @@ export class Editor extends EventEmitter<TLEventMap> {
10223
10286
  if (info.type === 'misc') {
10224
10287
  // stop panning if the interaction is cancelled or completed
10225
10288
  if (info.name === 'cancel' || info.name === 'complete') {
10226
- this.inputs.isDragging = false
10289
+ this.inputs.setIsDragging(false)
10227
10290
 
10228
- if (this.inputs.isPanning) {
10229
- this.inputs.isPanning = false
10230
- this.inputs.isSpacebarPanning = false
10291
+ if (this.inputs.getIsPanning()) {
10292
+ this.inputs.setIsPanning(false)
10293
+ this.inputs.setIsSpacebarPanning(false)
10231
10294
  this.setCursor({ type: this._prevCursor, rotation: 0 })
10232
10295
  }
10233
10296
  }
10234
10297
 
10235
10298
  this.root.handleEvent(info)
10299
+ this.emit('event', info)
10236
10300
  return
10237
10301
  }
10238
10302
 
10239
10303
  if (info.shiftKey) {
10240
10304
  clearTimeout(this._shiftKeyTimeout)
10241
10305
  this._shiftKeyTimeout = -1
10242
- inputs.shiftKey = true
10243
- } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
10306
+ inputs.setShiftKey(true)
10307
+ } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
10244
10308
  this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
10245
10309
  }
10246
10310
 
10247
10311
  if (info.altKey) {
10248
10312
  clearTimeout(this._altKeyTimeout)
10249
10313
  this._altKeyTimeout = -1
10250
- inputs.altKey = true
10251
- } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
10314
+ inputs.setAltKey(true)
10315
+ } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
10252
10316
  this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
10253
10317
  }
10254
10318
 
10255
10319
  if (info.ctrlKey) {
10256
10320
  clearTimeout(this._ctrlKeyTimeout)
10257
10321
  this._ctrlKeyTimeout = -1
10258
- inputs.ctrlKey = true
10259
- } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
10322
+ inputs.setCtrlKey(true)
10323
+ } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
10260
10324
  this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
10261
10325
  }
10262
10326
 
10263
10327
  if (info.metaKey) {
10264
10328
  clearTimeout(this._metaKeyTimeout)
10265
10329
  this._metaKeyTimeout = -1
10266
- inputs.metaKey = true
10267
- } else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
10330
+ inputs.setMetaKey(true)
10331
+ } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
10268
10332
  this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150)
10269
10333
  }
10270
10334
 
10271
- const { originPagePoint, currentPagePoint } = inputs
10272
-
10273
- if (!inputs.isPointing) {
10274
- inputs.isDragging = false
10335
+ if (!inputs.getIsPointing()) {
10336
+ inputs.setIsDragging(false)
10275
10337
  }
10276
10338
 
10277
10339
  const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -10282,29 +10344,29 @@ export class Editor extends EventEmitter<TLEventMap> {
10282
10344
  case 'pinch': {
10283
10345
  if (cameraOptions.isLocked) return
10284
10346
  clearTimeout(this._longPressTimeout)
10285
- this._updateInputsFromEvent(info)
10347
+ this.inputs.updateFromEvent(info)
10286
10348
 
10287
10349
  switch (info.name) {
10288
10350
  case 'pinch_start': {
10289
- if (inputs.isPinching) return
10351
+ if (inputs.getIsPinching()) return
10290
10352
 
10291
- if (!inputs.isEditing) {
10292
- this._pinchStart = this.getCamera().z
10353
+ if (!inputs.getIsEditing()) {
10293
10354
  if (!this._selectedShapeIdsAtPointerDown.length) {
10294
10355
  this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10295
10356
  }
10296
10357
 
10297
10358
  this._didPinch = true
10298
10359
 
10299
- inputs.isPinching = true
10360
+ inputs.setIsPinching(true)
10300
10361
 
10301
10362
  this.interrupt()
10302
10363
  }
10303
10364
 
10365
+ this.emit('event', info)
10304
10366
  return // Stop here!
10305
10367
  }
10306
10368
  case 'pinch': {
10307
- if (!inputs.isPinching) return
10369
+ if (!inputs.getIsPinching()) return
10308
10370
 
10309
10371
  const {
10310
10372
  point: { z = 1 },
@@ -10335,13 +10397,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10335
10397
  { immediate: true }
10336
10398
  )
10337
10399
 
10400
+ this.emit('event', info)
10338
10401
  return // Stop here!
10339
10402
  }
10340
10403
  case 'pinch_end': {
10341
- if (!inputs.isPinching) return this
10404
+ if (!inputs.getIsPinching()) return this
10342
10405
 
10343
10406
  // Stop pinching
10344
- inputs.isPinching = false
10407
+ inputs.setIsPinching(false)
10345
10408
 
10346
10409
  // Stash and clear the shapes that were selected when the pinch started
10347
10410
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
@@ -10361,6 +10424,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10361
10424
  }
10362
10425
  }
10363
10426
 
10427
+ this.emit('event', info)
10364
10428
  return // Stop here!
10365
10429
  }
10366
10430
  }
@@ -10368,7 +10432,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10368
10432
  case 'wheel': {
10369
10433
  if (cameraOptions.isLocked) return
10370
10434
 
10371
- this._updateInputsFromEvent(info)
10435
+ this.inputs.updateFromEvent(info)
10372
10436
 
10373
10437
  const { panSpeed, zoomSpeed } = cameraOptions
10374
10438
  let wheelBehavior = cameraOptions.wheelBehavior
@@ -10399,7 +10463,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10399
10463
  switch (behavior) {
10400
10464
  case 'zoom': {
10401
10465
  // Zoom in on current screen point using the wheel delta
10402
- const { x, y } = this.inputs.currentScreenPoint
10466
+ const { x, y } = this.inputs.getCurrentScreenPoint()
10403
10467
  let delta = dz
10404
10468
 
10405
10469
  // If we're forcing zoom, then we need to do the wheel normalization math here
@@ -10416,6 +10480,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10416
10480
  immediate: true,
10417
10481
  })
10418
10482
  this.maybeTrackPerformance('Zooming')
10483
+ this.root.handleEvent(info)
10484
+ this.emit('event', info)
10419
10485
  return
10420
10486
  }
10421
10487
  case 'pan': {
@@ -10424,6 +10490,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10424
10490
  immediate: true,
10425
10491
  })
10426
10492
  this.maybeTrackPerformance('Panning')
10493
+ this.root.handleEvent(info)
10494
+ this.emit('event', info)
10427
10495
  return
10428
10496
  }
10429
10497
  }
@@ -10432,9 +10500,9 @@ export class Editor extends EventEmitter<TLEventMap> {
10432
10500
  }
10433
10501
  case 'pointer': {
10434
10502
  // Ignore pointer events while we're pinching
10435
- if (inputs.isPinching) return
10503
+ if (inputs.getIsPinching()) return
10436
10504
 
10437
- this._updateInputsFromEvent(info)
10505
+ this.inputs.updateFromEvent(info)
10438
10506
  const { isPen } = info
10439
10507
  const { isPenMode } = instanceState
10440
10508
 
@@ -10443,7 +10511,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10443
10511
  // If we're in pen mode and the input is not a pen type, then stop here
10444
10512
  if (isPenMode && !isPen) return
10445
10513
 
10446
- if (!this.inputs.isPanning) {
10514
+ if (!this.inputs.getIsPanning()) {
10447
10515
  // Start a long press timeout
10448
10516
  this._longPressTimeout = this.timers.setTimeout(() => {
10449
10517
  const vsb = this.getViewportScreenBounds()
@@ -10453,7 +10521,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10453
10521
  // viewport bounds, and will be again when this event is handled...
10454
10522
  // so we need to counter-adjust from the stored value so that the
10455
10523
  // new value is set correctly.
10456
- point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
10524
+ point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y),
10457
10525
  name: 'long_press',
10458
10526
  })
10459
10527
  }, this.options.longPressDurationMs)
@@ -10470,8 +10538,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10470
10538
  inputs.buttons.add(info.button)
10471
10539
 
10472
10540
  // Start pointing and stop dragging
10473
- inputs.isPointing = true
10474
- inputs.isDragging = false
10541
+ inputs.setIsPointing(true)
10542
+ inputs.setIsDragging(false)
10475
10543
 
10476
10544
  // If pen mode is off but we're not already in pen mode, turn that on
10477
10545
  if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
@@ -10483,16 +10551,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10483
10551
  this.setCurrentTool('eraser')
10484
10552
  } else if (info.button === MIDDLE_MOUSE_BUTTON) {
10485
10553
  // Middle mouse pan activates panning unless we're already panning (with spacebar)
10486
- if (!this.inputs.isPanning) {
10554
+ if (!this.inputs.getIsPanning()) {
10487
10555
  this._prevCursor = this.getInstanceState().cursor.type
10488
10556
  }
10489
- this.inputs.isPanning = true
10557
+ this.inputs.setIsPanning(true)
10490
10558
  clearTimeout(this._longPressTimeout)
10491
10559
  }
10492
10560
 
10493
10561
  // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click
10494
10562
  // Also stop here, we don't want the state chart to receive the event
10495
- if (this.inputs.isPanning) {
10563
+ if (this.inputs.getIsPanning()) {
10496
10564
  this.stopCameraAnimation()
10497
10565
  this.setCursor({ type: 'grabbing', rotation: 0 })
10498
10566
  return this
@@ -10507,9 +10575,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10507
10575
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
10508
10576
 
10509
10577
  // If we've started panning, then clear any long press timeout
10510
- if (this.inputs.isPanning && this.inputs.isPointing) {
10578
+ if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) {
10511
10579
  // Handle spacebar / middle mouse button panning
10512
- const { currentScreenPoint, previousScreenPoint } = this.inputs
10580
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
10581
+ const previousScreenPoint = this.inputs.getPreviousScreenPoint()
10513
10582
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10514
10583
  this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10515
10584
  immediate: true,
@@ -10519,24 +10588,25 @@ export class Editor extends EventEmitter<TLEventMap> {
10519
10588
  }
10520
10589
 
10521
10590
  if (
10522
- inputs.isPointing &&
10523
- !inputs.isDragging &&
10524
- Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() >
10591
+ inputs.getIsPointing() &&
10592
+ !inputs.getIsDragging() &&
10593
+ Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) *
10594
+ this.getZoomLevel() >
10525
10595
  (instanceState.isCoarsePointer
10526
10596
  ? this.options.coarseDragDistanceSquared
10527
10597
  : this.options.dragDistanceSquared) /
10528
10598
  cz
10529
10599
  ) {
10530
10600
  // Start dragging
10531
- inputs.isDragging = true
10601
+ inputs.setIsDragging(true)
10532
10602
  clearTimeout(this._longPressTimeout)
10533
10603
  }
10534
10604
  break
10535
10605
  }
10536
10606
  case 'pointer_up': {
10537
10607
  // Stop dragging / pointing
10538
- inputs.isDragging = false
10539
- inputs.isPointing = false
10608
+ inputs.setIsDragging(false)
10609
+ inputs.setIsPointing(false)
10540
10610
  clearTimeout(this._longPressTimeout)
10541
10611
 
10542
10612
  // Remove the button from the buttons set
@@ -10553,12 +10623,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10553
10623
  info.button = 0
10554
10624
  }
10555
10625
 
10556
- if (inputs.isPanning) {
10626
+ if (inputs.getIsPanning()) {
10557
10627
  if (!inputs.keys.has('Space')) {
10558
- inputs.isPanning = false
10559
- inputs.isSpacebarPanning = false
10628
+ inputs.setIsPanning(false)
10629
+ inputs.setIsSpacebarPanning(false)
10560
10630
  }
10561
- const slideDirection = this.inputs.pointerVelocity
10631
+ const slideDirection = this.inputs.getPointerVelocity()
10562
10632
  const slideSpeed = Math.min(2, slideDirection.len())
10563
10633
 
10564
10634
  switch (info.button) {
@@ -10602,43 +10672,48 @@ export class Editor extends EventEmitter<TLEventMap> {
10602
10672
  // Add the key from the keys set
10603
10673
  inputs.keys.add(info.code)
10604
10674
 
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
- }
10675
+ if (this.options.spacebarPanning) {
10676
+ // If the space key is pressed (but meta / control isn't!) activate panning
10677
+ if (info.code === 'Space' && !info.ctrlKey) {
10678
+ if (!this.inputs.getIsPanning()) {
10679
+ this._prevCursor = instanceState.cursor.type
10680
+ }
10610
10681
 
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
- }
10682
+ this.inputs.setIsPanning(true)
10683
+ this.inputs.setIsSpacebarPanning(true)
10684
+ clearTimeout(this._longPressTimeout)
10685
+ this.setCursor({
10686
+ type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
10687
+ rotation: 0,
10688
+ })
10689
+ }
10616
10690
 
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
10691
+ if (this.inputs.getIsSpacebarPanning()) {
10692
+ let offset: Vec | undefined
10693
+ switch (info.code) {
10694
+ case 'ArrowUp': {
10695
+ offset = new Vec(0, -1)
10696
+ break
10697
+ }
10698
+ case 'ArrowRight': {
10699
+ offset = new Vec(1, 0)
10700
+ break
10701
+ }
10702
+ case 'ArrowDown': {
10703
+ offset = new Vec(0, 1)
10704
+ break
10705
+ }
10706
+ case 'ArrowLeft': {
10707
+ offset = new Vec(-1, 0)
10708
+ break
10709
+ }
10635
10710
  }
10636
- }
10637
10711
 
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 } })
10712
+ if (offset) {
10713
+ const bounds = this.getViewportPageBounds()
10714
+ const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10715
+ this._animateToViewport(next, { animation: { duration: 320 } })
10716
+ }
10642
10717
  }
10643
10718
  }
10644
10719
 
@@ -10648,15 +10723,17 @@ export class Editor extends EventEmitter<TLEventMap> {
10648
10723
  // Remove the key from the keys set
10649
10724
  inputs.keys.delete(info.code)
10650
10725
 
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 })
10726
+ if (this.options.spacebarPanning) {
10727
+ // If we've lifted the space key,
10728
+ if (info.code === 'Space') {
10729
+ if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10730
+ // If we're still middle dragging, continue panning
10731
+ } else {
10732
+ // otherwise, stop panning
10733
+ this.inputs.setIsPanning(false)
10734
+ this.inputs.setIsSpacebarPanning(false)
10735
+ this.setCursor({ type: this._prevCursor, rotation: 0 })
10736
+ }
10660
10737
  }
10661
10738
  }
10662
10739
  break
@@ -10730,7 +10807,10 @@ function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) {
10730
10807
 
10731
10808
  function applyPartialToRecordWithProps<
10732
10809
  T extends UnknownRecord & { type: string; props: object; meta: object },
10733
- >(prev: T, partial?: Partial<T> & { props?: Partial<T['props']> }): T {
10810
+ >(
10811
+ prev: T,
10812
+ partial?: T extends T ? Omit<Partial<T>, 'props'> & { props?: Partial<T['props']> } : never
10813
+ ): T {
10734
10814
  if (!partial) return prev
10735
10815
  let next = null as null | T
10736
10816
  const entries = Object.entries(partial)