@tldraw/editor 4.3.0-canary.2362fd2ebe56 → 4.3.0-canary.2643056dfc8d

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 (177) hide show
  1. package/dist-cjs/index.d.ts +537 -120
  2. package/dist-cjs/index.js +8 -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 +4 -5
  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 +349 -280
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -23
  14. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  22. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  23. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  27. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +181 -0
  28. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  29. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  30. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  32. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  34. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  35. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  36. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  37. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  38. package/dist-cjs/lib/exports/parseCss.js +1 -1
  39. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  40. package/dist-cjs/lib/globals/environment.js +45 -9
  41. package/dist-cjs/lib/globals/environment.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  43. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  44. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  45. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  48. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  49. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  52. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  53. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  54. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  55. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  56. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  57. package/dist-cjs/lib/options.js +6 -1
  58. package/dist-cjs/lib/options.js.map +2 -2
  59. package/dist-cjs/lib/primitives/Box.js +3 -0
  60. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  62. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  63. package/dist-cjs/lib/utils/rotation.js +1 -1
  64. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  65. package/dist-cjs/version.js +3 -3
  66. package/dist-cjs/version.js.map +1 -1
  67. package/dist-esm/index.d.mts +537 -120
  68. package/dist-esm/index.mjs +9 -2
  69. package/dist-esm/index.mjs.map +2 -2
  70. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  71. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  72. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  73. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -5
  74. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  75. package/dist-esm/lib/constants.mjs +1 -3
  76. package/dist-esm/lib/constants.mjs.map +2 -2
  77. package/dist-esm/lib/editor/Editor.mjs +350 -283
  78. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  79. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -23
  80. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  81. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  82. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  83. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  84. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  85. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  86. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  87. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  88. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  89. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  90. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  92. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  93. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +161 -0
  94. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  95. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  96. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  97. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  98. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  99. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  100. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  101. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  102. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  103. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  104. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  105. package/dist-esm/lib/globals/environment.mjs +45 -9
  106. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  107. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  108. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  109. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  110. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  111. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  112. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  113. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  114. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  115. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  116. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  117. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  118. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  119. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  120. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  121. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  122. package/dist-esm/lib/options.mjs +6 -1
  123. package/dist-esm/lib/options.mjs.map +2 -2
  124. package/dist-esm/lib/primitives/Box.mjs +3 -0
  125. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  126. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  127. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  128. package/dist-esm/lib/utils/rotation.mjs +1 -1
  129. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  130. package/dist-esm/version.mjs +3 -3
  131. package/dist-esm/version.mjs.map +1 -1
  132. package/editor.css +14 -12
  133. package/package.json +21 -17
  134. package/src/index.ts +5 -1
  135. package/src/lib/components/ErrorBoundary.tsx +1 -1
  136. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  137. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -8
  138. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  139. package/src/lib/constants.ts +0 -2
  140. package/src/lib/editor/Editor.test.ts +140 -0
  141. package/src/lib/editor/Editor.ts +455 -326
  142. package/src/lib/editor/derivations/notVisibleShapes.ts +21 -33
  143. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  144. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  145. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  146. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  147. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  148. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  149. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  150. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  151. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  152. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  153. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +215 -0
  154. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  155. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  156. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  157. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  158. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  159. package/src/lib/editor/types/emit-types.ts +3 -1
  160. package/src/lib/exports/parseCss.test.ts +1 -0
  161. package/src/lib/exports/parseCss.ts +1 -1
  162. package/src/lib/globals/environment.ts +65 -10
  163. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  164. package/src/lib/hooks/useEvent.tsx +1 -1
  165. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  166. package/src/lib/hooks/useGestureEvents.ts +2 -2
  167. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  168. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  169. package/src/lib/hooks/useScreenBounds.ts +1 -1
  170. package/src/lib/hooks/useStateAttribute.ts +4 -1
  171. package/src/lib/hooks/useTransform.ts +1 -1
  172. package/src/lib/hooks/useZoomCss.ts +3 -8
  173. package/src/lib/options.ts +32 -0
  174. package/src/lib/primitives/Box.ts +9 -0
  175. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  176. package/src/lib/utils/rotation.ts +1 -1
  177. package/src/version.ts +3 -3
@@ -42,7 +42,6 @@ import {
42
42
  TLInstance,
43
43
  TLInstancePageState,
44
44
  TLInstancePresence,
45
- TLPOINTER_ID,
46
45
  TLPage,
47
46
  TLPageId,
48
47
  TLParentId,
@@ -109,7 +108,6 @@ import {
109
108
  MIDDLE_MOUSE_BUTTON,
110
109
  RIGHT_MOUSE_BUTTON,
111
110
  STYLUS_ERASER_BUTTON,
112
- ZOOM_TO_FIT_PADDING,
113
111
  } from '../constants'
114
112
  import { exportToSvg } from '../exports/exportToSvg'
115
113
  import { getSvgAsImage } from '../exports/getSvgAsImage'
@@ -135,7 +133,6 @@ import {
135
133
  parseDeepLinkString,
136
134
  } from '../utils/deepLinks'
137
135
  import { getIncrementedName } from '../utils/getIncrementedName'
138
- import { isAccelKey } from '../utils/keyboard'
139
136
  import { getReorderingShapesChanges } from '../utils/reorderShapes'
140
137
  import { TLTextOptions, TiptapEditor } from '../utils/richText'
141
138
  import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
@@ -149,22 +146,19 @@ import { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManage
149
146
  import { FocusManager } from './managers/FocusManager/FocusManager'
150
147
  import { FontManager } from './managers/FontManager/FontManager'
151
148
  import { HistoryManager } from './managers/HistoryManager/HistoryManager'
149
+ import { InputsManager } from './managers/InputsManager/InputsManager'
152
150
  import { ScribbleManager } from './managers/ScribbleManager/ScribbleManager'
153
151
  import { SnapManager } from './managers/SnapManager/SnapManager'
152
+ import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndexManager'
154
153
  import { TextManager } from './managers/TextManager/TextManager'
155
154
  import { TickManager } from './managers/TickManager/TickManager'
156
155
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
157
- import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
156
+ import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
158
157
  import { RootState } from './tools/RootState'
159
158
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
160
159
  import { TLContent } from './types/clipboard-types'
161
160
  import { TLEventMap } from './types/emit-types'
162
- import {
163
- TLEventInfo,
164
- TLPinchEventInfo,
165
- TLPointerEventInfo,
166
- TLWheelEventInfo,
167
- } from './types/event-types'
161
+ import { TLEventInfo, TLPointerEventInfo } from './types/event-types'
168
162
  import { TLExternalAsset, TLExternalContent } from './types/external-content'
169
163
  import { TLHistoryBatchOptions } from './types/history-types'
170
164
  import {
@@ -195,7 +189,7 @@ export type TLResizeShapeOptions = Partial<{
195
189
  /** @public */
196
190
  export interface TLEditorOptions {
197
191
  /**
198
- * 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
199
193
  * from a server or database.
200
194
  */
201
195
  store: TLStore
@@ -315,6 +309,9 @@ export class Editor extends EventEmitter<TLEventMap> {
315
309
 
316
310
  this.snaps = new SnapManager(this)
317
311
 
312
+ this.spatialIndex = new SpatialIndexManager(this)
313
+ this.disposables.add(() => this.spatialIndex.dispose())
314
+
318
315
  this.disposables.add(this.timers.dispose)
319
316
 
320
317
  this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
@@ -333,6 +330,8 @@ export class Editor extends EventEmitter<TLEventMap> {
333
330
 
334
331
  this._tickManager = new TickManager(this)
335
332
 
333
+ this.inputs = new InputsManager(this)
334
+
336
335
  class NewRoot extends RootState {
337
336
  static override initial = initialState ?? ''
338
337
  }
@@ -867,7 +866,7 @@ export class Editor extends EventEmitter<TLEventMap> {
867
866
  }
868
867
 
869
868
  /**
870
- * A set of functions to call when the app is disposed.
869
+ * A set of functions to call when the editor is disposed.
871
870
  *
872
871
  * @public
873
872
  */
@@ -880,16 +879,33 @@ export class Editor extends EventEmitter<TLEventMap> {
880
879
  */
881
880
  isDisposed = false
882
881
 
883
- /** @internal */
884
- private readonly _tickManager
882
+ /**
883
+ * A manager for the editor's tick events.
884
+ *
885
+ * @internal */
886
+ private readonly _tickManager: TickManager
885
887
 
886
888
  /**
887
- * 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.
888
897
  *
889
898
  * @public
890
899
  */
891
900
  readonly snaps: SnapManager
892
901
 
902
+ /**
903
+ * A manager for spatial indexing, enabling efficient shape location queries.
904
+ *
905
+ * @public
906
+ */
907
+ readonly spatialIndex: SpatialIndexManager
908
+
893
909
  /**
894
910
  * A manager for the any asynchronous events and making sure they're
895
911
  * cleaned up upon disposal.
@@ -969,6 +985,7 @@ export class Editor extends EventEmitter<TLEventMap> {
969
985
  this.disposables.clear()
970
986
  this.store.dispose()
971
987
  this.isDisposed = true
988
+ this.emit('dispose')
972
989
  }
973
990
 
974
991
  /* ------------------- Shape Utils ------------------ */
@@ -1060,7 +1077,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1060
1077
  /* --------------------- History -------------------- */
1061
1078
 
1062
1079
  /**
1063
- * A manager for the app's history.
1080
+ * A manager for the editor's history.
1064
1081
  *
1065
1082
  * @readonly
1066
1083
  */
@@ -1084,14 +1101,18 @@ export class Editor extends EventEmitter<TLEventMap> {
1084
1101
  }
1085
1102
 
1086
1103
  /**
1087
- * Whether the app can undo.
1104
+ * Whether the editor can undo.
1088
1105
  *
1089
1106
  * @public
1090
1107
  */
1091
- @computed getCanUndo(): boolean {
1108
+ @computed canUndo(): boolean {
1092
1109
  return this.history.getNumUndos() > 0
1093
1110
  }
1094
1111
 
1112
+ getCanUndo() {
1113
+ return this.canUndo()
1114
+ }
1115
+
1095
1116
  /**
1096
1117
  * Redo to the next mark.
1097
1118
  *
@@ -1109,20 +1130,24 @@ export class Editor extends EventEmitter<TLEventMap> {
1109
1130
  return this
1110
1131
  }
1111
1132
 
1112
- clearHistory() {
1113
- this.history.clear()
1114
- return this
1115
- }
1116
-
1117
1133
  /**
1118
- * Whether the app can redo.
1134
+ * Whether the editor can redo.
1119
1135
  *
1120
1136
  * @public
1121
1137
  */
1122
- @computed getCanRedo(): boolean {
1138
+ @computed canRedo(): boolean {
1123
1139
  return this.history.getNumRedos() > 0
1124
1140
  }
1125
1141
 
1142
+ getCanRedo() {
1143
+ return this.canRedo()
1144
+ }
1145
+
1146
+ clearHistory() {
1147
+ this.history.clear()
1148
+ return this
1149
+ }
1150
+
1126
1151
  /**
1127
1152
  * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
1128
1153
  * any redos. You typically want to do this just before a user interaction begins or is handled.
@@ -1296,7 +1321,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1296
1321
  }),
1297
1322
  selectionCount: this.getSelectedShapes().length,
1298
1323
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1299
- inputs: this.inputs,
1324
+ inputs: this.inputs.toJson(),
1300
1325
  pageState: this.getCurrentPageState(),
1301
1326
  instanceState: this.getInstanceState(),
1302
1327
  collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
@@ -1321,7 +1346,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1321
1346
  * we're in a transaction that's about to be rolled back due to the same error we're currently
1322
1347
  * reporting.
1323
1348
  *
1324
- * Instead, to listen to changes to this value, you need to listen to app's `crash` event.
1349
+ * Instead, to listen to changes to this value, you need to listen to editor's `crash` event.
1325
1350
  *
1326
1351
  * @internal
1327
1352
  */
@@ -2024,7 +2049,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2024
2049
  }
2025
2050
 
2026
2051
  /**
2027
- * The id of the app's only selected shape.
2052
+ * The id of the editor's only selected shape.
2028
2053
  *
2029
2054
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
2030
2055
  *
@@ -2036,7 +2061,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2036
2061
  }
2037
2062
 
2038
2063
  /**
2039
- * The app's only selected shape.
2064
+ * The editor's only selected shape.
2040
2065
  *
2041
2066
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
2042
2067
  *
@@ -2277,6 +2302,29 @@ export class Editor extends EventEmitter<TLEventMap> {
2277
2302
  return editingShapeId ? this.getShape(editingShapeId) : undefined
2278
2303
  }
2279
2304
 
2305
+ /**
2306
+ * Whether the shape can be edited.
2307
+ *
2308
+ * @param shape - The shape (or shape id) to check if it can be edited.
2309
+ * @param info - The info about the edit start.
2310
+ *
2311
+ * @public
2312
+ * @returns true if the shape can be edited, false otherwise.
2313
+ */
2314
+ canEditShape<T extends TLShape | TLShapeId>(shape: T | null, info?: TLEditStartInfo): shape is T {
2315
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2316
+ if (!id) return false // no shape
2317
+ if (id === this.getEditingShapeId()) return false // already editing this shape
2318
+ const _shape = this.getShape(id)
2319
+ if (!_shape) return false // no shape
2320
+ const util = this.getShapeUtil(_shape)
2321
+ const _info: TLEditStartInfo = info ?? { type: 'unknown' }
2322
+ if (!util.canEdit(_shape, _info)) return false // shape is not editable
2323
+ if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception
2324
+ 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.
2325
+ return true // shape is editable
2326
+ }
2327
+
2280
2328
  /**
2281
2329
  * Set the current editing shape.
2282
2330
  *
@@ -2292,44 +2340,59 @@ export class Editor extends EventEmitter<TLEventMap> {
2292
2340
  */
2293
2341
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2294
2342
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2295
- this.setRichTextEditor(null)
2296
- const prevEditingShapeId = this.getEditingShapeId()
2297
- if (id !== prevEditingShapeId) {
2298
- if (id) {
2299
- const shape = this.getShape(id)
2300
- if (shape && this.getShapeUtil(shape).canEdit(shape)) {
2301
- this.run(
2302
- () => {
2303
- this._updateCurrentPageState({ editingShapeId: id })
2304
- if (prevEditingShapeId) {
2305
- const prevEditingShape = this.getShape(prevEditingShapeId)
2306
- if (prevEditingShape) {
2307
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2308
- }
2309
- }
2310
- this.getShapeUtil(shape).onEditStart?.(shape)
2311
- },
2312
- { history: 'ignore' }
2313
- )
2314
- return this
2315
- }
2316
- }
2317
2343
 
2318
- // Either we just set the editing id to null, or the shape was missing or not editable
2344
+ if (!id) {
2345
+ // setting the editing shape to null
2319
2346
  this.run(
2320
2347
  () => {
2321
- this._updateCurrentPageState({ editingShapeId: null })
2322
- this._currentRichTextEditor.set(null)
2348
+ // Clean up the previous editing shape
2349
+ const prevEditingShapeId = this.getEditingShapeId()
2323
2350
  if (prevEditingShapeId) {
2324
2351
  const prevEditingShape = this.getShape(prevEditingShapeId)
2325
2352
  if (prevEditingShape) {
2326
2353
  this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2327
2354
  }
2328
2355
  }
2356
+
2357
+ // Clean up the editing shape state and rich text editor
2358
+ this._updateCurrentPageState({ editingShapeId: null })
2359
+ this._currentRichTextEditor.set(null)
2329
2360
  },
2330
2361
  { history: 'ignore' }
2331
2362
  )
2363
+
2364
+ return this
2332
2365
  }
2366
+
2367
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2368
+ if (!this.canEditShape(id)) return this
2369
+
2370
+ // id was provided and the next editing shape is editable, so set the rich text editor to null
2371
+ this.run(
2372
+ () => {
2373
+ // Clean up the previous editing shape
2374
+ const prevEditingShapeId = this.getEditingShapeId()
2375
+ if (prevEditingShapeId) {
2376
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2377
+ if (prevEditingShape) {
2378
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2379
+ }
2380
+ }
2381
+
2382
+ // Clean up the editing shape state and rich text editor
2383
+ this._updateCurrentPageState({ editingShapeId: null })
2384
+ this._currentRichTextEditor.set(null)
2385
+
2386
+ // Set the new editing shape
2387
+ this.select(id)
2388
+ this._updateCurrentPageState({ editingShapeId: id })
2389
+
2390
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2391
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2392
+ },
2393
+ { history: 'ignore' }
2394
+ )
2395
+
2333
2396
  return this
2334
2397
  }
2335
2398
 
@@ -2533,6 +2596,26 @@ export class Editor extends EventEmitter<TLEventMap> {
2533
2596
  return this.getCurrentPageState().croppingShapeId
2534
2597
  }
2535
2598
 
2599
+ /**
2600
+ * Whether the shape can be cropped.
2601
+ *
2602
+ * @param shape - The shape (or shape id) to check if it can be cropped.
2603
+ *
2604
+ * @public
2605
+ * @returns true if the shape can be cropped, false otherwise.
2606
+ */
2607
+ canCropShape<T extends TLShape | TLShapeId>(shape: T | null): shape is T {
2608
+ if (!shape) return false
2609
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2610
+ if (!id) return false
2611
+ const _shape = this.getShape(id)
2612
+ if (!_shape) return false
2613
+ const util = this.getShapeUtil(_shape)
2614
+ if (!util.canCrop(_shape)) return false
2615
+ if (this.isShapeOrAncestorLocked(_shape)) return false
2616
+ return true
2617
+ }
2618
+
2536
2619
  /**
2537
2620
  * Set the current cropping shape.
2538
2621
  *
@@ -2554,12 +2637,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2554
2637
  () => {
2555
2638
  if (!id) {
2556
2639
  this.updateCurrentPageState({ croppingShapeId: null })
2557
- } else {
2558
- const shape = this.getShape(id)!
2559
- const util = this.getShapeUtil(shape)
2560
- if (shape && util.canCrop(shape)) {
2561
- this.updateCurrentPageState({ croppingShapeId: id })
2562
- }
2640
+ } else if (this.canCropShape(id)) {
2641
+ this.updateCurrentPageState({ croppingShapeId: id })
2563
2642
  }
2564
2643
  },
2565
2644
  { history: 'ignore' }
@@ -2669,6 +2748,52 @@ export class Editor extends EventEmitter<TLEventMap> {
2669
2748
  return this.getCamera().z
2670
2749
  }
2671
2750
 
2751
+ private _debouncedZoomLevel = atom('debounced zoom level', 1)
2752
+
2753
+ /**
2754
+ * Get the debounced zoom level. When the camera is moving, this returns the zoom level
2755
+ * from when the camera started moving rather than the current zoom level. This can be
2756
+ * used to avoid expensive re-renders during camera movements.
2757
+ *
2758
+ * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom`
2759
+ * is `false`, this method always returns the current zoom level.
2760
+ *
2761
+ * @public
2762
+ */
2763
+ @computed getDebouncedZoomLevel() {
2764
+ if (this.options.debouncedZoom) {
2765
+ if (this.getCameraState() === 'idle') {
2766
+ return this.getZoomLevel()
2767
+ } else {
2768
+ return this._debouncedZoomLevel.get()
2769
+ }
2770
+ }
2771
+
2772
+ return this.getZoomLevel()
2773
+ }
2774
+
2775
+ @computed private _getAboveDebouncedZoomThreshold() {
2776
+ return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold
2777
+ }
2778
+
2779
+ /**
2780
+ * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2781
+ * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2782
+ *
2783
+ * @public
2784
+ * @example
2785
+ * ```ts
2786
+ * editor.getEfficientZoomLevel()
2787
+ * ```
2788
+ *
2789
+ * @public
2790
+ */
2791
+ @computed getEfficientZoomLevel() {
2792
+ return this._getAboveDebouncedZoomThreshold()
2793
+ ? this.getDebouncedZoomLevel()
2794
+ : this.getZoomLevel()
2795
+ }
2796
+
2672
2797
  /**
2673
2798
  * Get the camera's initial or reset zoom level.
2674
2799
  *
@@ -2995,7 +3120,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2995
3120
 
2996
3121
  // Dispatch a new pointer move because the pointer's page will have changed
2997
3122
  // (its screen position will compute to a new page position given the new camera position)
2998
- const { currentScreenPoint, currentPagePoint } = this.inputs
3123
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
3124
+ const currentPagePoint = this.inputs.getCurrentPagePoint()
2999
3125
 
3000
3126
  // compare the next page point (derived from the current camera) to the current page point
3001
3127
  if (
@@ -3159,7 +3285,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3159
3285
  * ```ts
3160
3286
  * editor.zoomIn()
3161
3287
  * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
3162
- * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
3288
+ * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
3163
3289
  * ```
3164
3290
  *
3165
3291
  * @param point - The screen point to zoom in on. Defaults to the screen center
@@ -3204,7 +3330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3204
3330
  * ```ts
3205
3331
  * editor.zoomOut()
3206
3332
  * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
3207
- * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
3333
+ * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } })
3208
3334
  * ```
3209
3335
  *
3210
3336
  * @param point - The point to zoom out on. Defaults to the viewport screen center.
@@ -3261,10 +3387,17 @@ export class Editor extends EventEmitter<TLEventMap> {
3261
3387
 
3262
3388
  const selectionPageBounds = this.getSelectionPageBounds()
3263
3389
  if (selectionPageBounds) {
3264
- this.zoomToBounds(selectionPageBounds, {
3265
- targetZoom: Math.max(1, this.getZoomLevel()),
3266
- ...opts,
3267
- })
3390
+ const currentZoom = this.getZoomLevel()
3391
+ // If already at 100%, zoom to fit the selection in the viewport
3392
+ // Otherwise, zoom to 100% centered on the selection
3393
+ if (Math.abs(currentZoom - 1) < 0.01) {
3394
+ this.zoomToBounds(selectionPageBounds, opts)
3395
+ } else {
3396
+ this.zoomToBounds(selectionPageBounds, {
3397
+ targetZoom: 1,
3398
+ ...opts,
3399
+ })
3400
+ }
3268
3401
  }
3269
3402
  return this
3270
3403
  }
@@ -3321,7 +3454,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3321
3454
 
3322
3455
  const viewportScreenBounds = this.getViewportScreenBounds()
3323
3456
 
3324
- const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28)
3457
+ const inset =
3458
+ opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28)
3325
3459
 
3326
3460
  const baseZoom = this.getBaseZoom()
3327
3461
  const zoomMin = cameraOptions.zoomSteps[0]
@@ -3631,22 +3765,23 @@ export class Editor extends EventEmitter<TLEventMap> {
3631
3765
  if (_willSetInitialBounds) {
3632
3766
  // If we have just received the initial bounds, don't center the camera.
3633
3767
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3768
+ this.emit('resize', screenBounds.toJson())
3634
3769
  this.setCamera(this.getCamera())
3635
3770
  } else {
3636
3771
  if (center && !this.getInstanceState().followingUserId) {
3637
3772
  // Get the page center before the change, make the change, and restore it
3638
3773
  const before = this.getViewportPageBounds().center
3639
3774
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3775
+ this.emit('resize', screenBounds.toJson())
3640
3776
  this.centerOnPoint(before)
3641
3777
  } else {
3642
3778
  // Otherwise,
3643
3779
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3780
+ this.emit('resize', screenBounds.toJson())
3644
3781
  this._setCamera(Vec.From({ ...this.getCamera() }))
3645
3782
  }
3646
3783
  }
3647
3784
 
3648
- this._tickCameraState()
3649
-
3650
3785
  return this
3651
3786
  }
3652
3787
 
@@ -4052,18 +4187,19 @@ export class Editor extends EventEmitter<TLEventMap> {
4052
4187
  // box just for rendering, and we only update after the camera stops moving.
4053
4188
  private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4054
4189
  private _cameraStateTimeoutRemaining = 0
4055
- _decayCameraStateTimeout(elapsed: number) {
4190
+ private _decayCameraStateTimeout(elapsed: number) {
4056
4191
  this._cameraStateTimeoutRemaining -= elapsed
4057
4192
  if (this._cameraStateTimeoutRemaining > 0) return
4058
4193
  this.off('tick', this._decayCameraStateTimeout)
4059
4194
  this._cameraState.set('idle')
4060
4195
  }
4061
- _tickCameraState() {
4196
+ private _tickCameraState() {
4062
4197
  // always reset the timeout
4063
4198
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4064
4199
  // If the state is idle, then start the tick
4065
4200
  if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4066
4201
  this._cameraState.set('moving')
4202
+ this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4067
4203
  this.on('tick', this._decayCameraStateTimeout)
4068
4204
  }
4069
4205
 
@@ -5010,6 +5146,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5010
5146
  }
5011
5147
 
5012
5148
  private _notVisibleShapes = notVisibleShapes(this)
5149
+ private _culledShapesCache: Set<TLShapeId> | null = null
5013
5150
 
5014
5151
  /**
5015
5152
  * Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
@@ -5021,16 +5158,41 @@ export class Editor extends EventEmitter<TLEventMap> {
5021
5158
  const notVisibleShapes = this.getNotVisibleShapes()
5022
5159
  const selectedShapeIds = this.getSelectedShapeIds()
5023
5160
  const editingId = this.getEditingShapeId()
5024
- const culledShapes = new Set<TLShapeId>(notVisibleShapes)
5161
+ const nextValue = new Set<TLShapeId>(notVisibleShapes)
5025
5162
  // we don't cull the shape we are editing
5026
5163
  if (editingId) {
5027
- culledShapes.delete(editingId)
5164
+ nextValue.delete(editingId)
5028
5165
  }
5029
5166
  // we also don't cull selected shapes
5030
5167
  selectedShapeIds.forEach((id) => {
5031
- culledShapes.delete(id)
5168
+ nextValue.delete(id)
5032
5169
  })
5033
- return culledShapes
5170
+
5171
+ // Cache optimization: return same Set object if contents unchanged
5172
+ // This allows consumers to use === comparison and prevents unnecessary re-renders
5173
+ const prevValue = this._culledShapesCache
5174
+ if (prevValue) {
5175
+ // If sizes differ, contents must differ
5176
+ if (prevValue.size !== nextValue.size) {
5177
+ this._culledShapesCache = nextValue
5178
+ return nextValue
5179
+ }
5180
+
5181
+ // Check if all elements are the same
5182
+ for (const id of prevValue) {
5183
+ if (!nextValue.has(id)) {
5184
+ // Found a difference, update cache and return new set
5185
+ this._culledShapesCache = nextValue
5186
+ return nextValue
5187
+ }
5188
+ }
5189
+
5190
+ // Loop completed without finding differences - contents identical
5191
+ return prevValue
5192
+ }
5193
+
5194
+ this._culledShapesCache = nextValue
5195
+ return nextValue
5034
5196
  }
5035
5197
 
5036
5198
  /**
@@ -5097,11 +5259,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5097
5259
  let inMarginClosestToEdgeDistance = Infinity
5098
5260
  let inMarginClosestToEdgeHit: TLShape | null = null
5099
5261
 
5262
+ // Use larger margin for spatial search to account for edge distance checks
5263
+ const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
5264
+ const candidateIds = this.spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5265
+
5100
5266
  const shapesToCheck = (
5101
5267
  opts.renderingOnly
5102
5268
  ? this.getCurrentPageRenderingShapesSorted()
5103
5269
  : this.getCurrentPageShapesSorted()
5104
5270
  ).filter((shape) => {
5271
+ // Frames have labels positioned above the shape (outside bounds), so always include them
5272
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5273
+
5105
5274
  if (
5106
5275
  (shape.isLocked && !hitLocked) ||
5107
5276
  this.isShapeHidden(shape) ||
@@ -5287,11 +5456,39 @@ export class Editor extends EventEmitter<TLEventMap> {
5287
5456
  point: VecLike,
5288
5457
  opts = {} as { margin?: number; hitInside?: boolean }
5289
5458
  ): TLShape[] {
5459
+ const margin = opts.margin ?? 0
5460
+ const candidateIds = this.spatialIndex.getShapeIdsAtPoint(point, margin)
5461
+
5462
+ // Get all page shapes in z-index order and filter to candidates that pass isPointInShape
5463
+ // Frames are always checked because their labels can be outside their bounds
5290
5464
  return this.getCurrentPageShapesSorted()
5291
- .filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
5465
+ .filter((shape) => {
5466
+ if (this.isShapeHidden(shape)) return false
5467
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5468
+ return this.isPointInShape(shape, point, opts)
5469
+ })
5292
5470
  .reverse()
5293
5471
  }
5294
5472
 
5473
+ /**
5474
+ * Get shape IDs within the given bounds.
5475
+ *
5476
+ * Note: Results are unordered. If you need z-order, combine with sorted shapes:
5477
+ * ```ts
5478
+ * const candidates = editor.getShapeIdsInsideBounds(bounds)
5479
+ * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
5480
+ * ```
5481
+ *
5482
+ * @param bounds - The bounds to search within.
5483
+ *
5484
+ * @returns Unordered set of shape IDs within the given bounds.
5485
+ *
5486
+ * @public
5487
+ */
5488
+ getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
5489
+ return this.spatialIndex.getShapeIdsInsideBounds(bounds)
5490
+ }
5491
+
5295
5492
  /**
5296
5493
  * Test whether a point (in the current page space) will will a shape. This method takes into account masks,
5297
5494
  * such as when a shape is the child of a frame and is partially clipped by the frame.
@@ -7649,8 +7846,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7649
7846
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
7650
7847
  // to make sure the shape is mirrored correctly
7651
7848
  if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
7652
- let { rotation } = Mat.Decompose(options.initialPageTransform)
7653
- rotation -= 2 * rotation
7849
+ // We need to compute the new local rotation that will result in the negated page rotation.
7850
+ // For a shape with local rotation `localRot` and parent page rotation `parentRot`:
7851
+ // - pageRot = parentRot + localRot
7852
+ // - newPageRot = -pageRot (we want to negate the page rotation)
7853
+ // - newPageRot = parentRot + newLocalRot (parent hasn't changed)
7854
+ // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot
7855
+ const parentRotation = this.getShapeParentTransform(id).rotation()
7856
+ const rotation = -options.initialShape.rotation - 2 * parentRotation
7654
7857
  this.updateShapes([{ id, type, rotation }])
7655
7858
  }
7656
7859
 
@@ -7670,9 +7873,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7670
7873
  )
7671
7874
 
7672
7875
  // now calculate how far away the shape is from where it needs to be
7673
- const pageBounds = this.getShapePageBounds(id)!
7674
7876
  const pageTransform = this.getShapePageTransform(id)!
7675
- const currentPageCenter = pageBounds.center
7877
+ // We need to use the local bounds center transformed to page space, not the axis-aligned
7878
+ // page bounds center. This is because the page bounds are axis-aligned and their center
7879
+ // changes when the rotation changes, but we want to use the same reference point as
7880
+ // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform).
7881
+ const currentLocalBounds = this.getShapeGeometry(id).bounds
7882
+ const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
7676
7883
  const shapePageTransformOrigin = pageTransform.point()
7677
7884
  if (!currentPageCenter || !shapePageTransformOrigin) return this
7678
7885
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
@@ -8120,7 +8327,12 @@ export class Editor extends EventEmitter<TLEventMap> {
8120
8327
  )
8121
8328
  )
8122
8329
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
8123
- const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
8330
+ const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
8331
+ const pageBounds = Box.Common(childBounds)
8332
+
8333
+ if (!pageBounds.isValid()) {
8334
+ throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`)
8335
+ }
8124
8336
 
8125
8337
  const { x, y } = pageBounds.point
8126
8338
 
@@ -9145,6 +9357,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9145
9357
  }
9146
9358
  }
9147
9359
 
9360
+ if (point) {
9361
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9362
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9363
+ if (rootShapesFromContent.length > 0) {
9364
+ const targetParent = this.getShapeAtPoint(point, {
9365
+ hitInside: true,
9366
+ hitFrameInside: true,
9367
+ hitLocked: true,
9368
+ filter: (shape) => {
9369
+ const util = this.getShapeUtil(shape)
9370
+ if (!util.canReceiveNewChildrenOfType) return false
9371
+ return rootShapesFromContent.every((rootShape) =>
9372
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9373
+ )
9374
+ },
9375
+ })
9376
+
9377
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9378
+ // parent under the pointer so that we don't keep using the original
9379
+ // selection's parent (which can keep shapes clipped inside frames).
9380
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9381
+ }
9382
+ }
9383
+
9148
9384
  let isDuplicating = false
9149
9385
 
9150
9386
  if (!isPageId(pasteParentId)) {
@@ -9482,126 +9718,6 @@ export class Editor extends EventEmitter<TLEventMap> {
9482
9718
 
9483
9719
  /* --------------------- Events --------------------- */
9484
9720
 
9485
- /**
9486
- * The app's current input state.
9487
- *
9488
- * @public
9489
- */
9490
- inputs = {
9491
- /** The most recent pointer down's position in the current page space. */
9492
- originPagePoint: new Vec(),
9493
- /** The most recent pointer down's position in screen space. */
9494
- originScreenPoint: new Vec(),
9495
- /** The previous pointer position in the current page space. */
9496
- previousPagePoint: new Vec(),
9497
- /** The previous pointer position in screen space. */
9498
- previousScreenPoint: new Vec(),
9499
- /** The most recent pointer position in the current page space. */
9500
- currentPagePoint: new Vec(),
9501
- /** The most recent pointer position in screen space. */
9502
- currentScreenPoint: new Vec(),
9503
- /** A set containing the currently pressed keys. */
9504
- keys: new Set<string>(),
9505
- /** A set containing the currently pressed buttons. */
9506
- buttons: new Set<number>(),
9507
- /** Whether the input is from a pe. */
9508
- isPen: false,
9509
- /** Whether the shift key is currently pressed. */
9510
- shiftKey: false,
9511
- /** Whether the meta key is currently pressed. */
9512
- metaKey: false,
9513
- /** Whether the control or command key is currently pressed. */
9514
- ctrlKey: false,
9515
- /** Whether the alt or option key is currently pressed. */
9516
- altKey: false,
9517
- /** Whether the user is dragging. */
9518
- isDragging: false,
9519
- /** Whether the user is pointing. */
9520
- isPointing: false,
9521
- /** Whether the user is pinching. */
9522
- isPinching: false,
9523
- /** Whether the user is editing. */
9524
- isEditing: false,
9525
- /** Whether the user is panning. */
9526
- isPanning: false,
9527
- /** Whether the user is spacebar panning. */
9528
- isSpacebarPanning: false,
9529
- /** Velocity of mouse pointer, in pixels per millisecond */
9530
- pointerVelocity: new Vec(),
9531
- }
9532
-
9533
- /**
9534
- * Update the input points from a pointer, pinch, or wheel event.
9535
- *
9536
- * @param info - The event info.
9537
- */
9538
- private _updateInputsFromEvent(
9539
- info: TLPointerEventInfo | TLPinchEventInfo | TLWheelEventInfo
9540
- ): void {
9541
- const {
9542
- pointerVelocity,
9543
- previousScreenPoint,
9544
- previousPagePoint,
9545
- currentScreenPoint,
9546
- currentPagePoint,
9547
- originScreenPoint,
9548
- originPagePoint,
9549
- } = this.inputs
9550
-
9551
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
9552
- const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
9553
-
9554
- const sx = info.point.x - screenBounds.x
9555
- const sy = info.point.y - screenBounds.y
9556
- const sz = info.point.z ?? 0.5
9557
-
9558
- previousScreenPoint.setTo(currentScreenPoint)
9559
- previousPagePoint.setTo(currentPagePoint)
9560
-
9561
- // The "screen bounds" is relative to the user's actual screen.
9562
- // The "screen point" is relative to the "screen bounds";
9563
- // it will be 0,0 when its actual screen position is equal
9564
- // to screenBounds.point. This is confusing!
9565
- currentScreenPoint.set(sx, sy)
9566
- const nx = sx / cz - cx
9567
- const ny = sy / cz - cy
9568
- if (isFinite(nx) && isFinite(ny)) {
9569
- currentPagePoint.set(nx, ny, sz)
9570
- }
9571
-
9572
- this.inputs.isPen = info.type === 'pointer' && info.isPen
9573
-
9574
- // Reset velocity on pointer down, or when a pinch starts or ends
9575
- if (info.name === 'pointer_down' || this.inputs.isPinching) {
9576
- pointerVelocity.set(0, 0)
9577
- originScreenPoint.setTo(currentScreenPoint)
9578
- originPagePoint.setTo(currentPagePoint)
9579
- }
9580
-
9581
- // todo: We only have to do this if there are multiple users in the document
9582
- this.run(
9583
- () => {
9584
- this.store.put([
9585
- {
9586
- id: TLPOINTER_ID,
9587
- typeName: 'pointer',
9588
- x: currentPagePoint.x,
9589
- y: currentPagePoint.y,
9590
- lastActivityTimestamp:
9591
- // If our pointer moved only because we're following some other user, then don't
9592
- // update our last activity timestamp; otherwise, update it to the current timestamp.
9593
- info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
9594
- ? (this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
9595
- this._tickManager.now)
9596
- : this._tickManager.now,
9597
- meta: {},
9598
- },
9599
- ])
9600
- },
9601
- { history: 'ignore' }
9602
- )
9603
- }
9604
-
9605
9721
  /**
9606
9722
  * Dispatch a cancel event.
9607
9723
  *
@@ -9671,19 +9787,22 @@ export class Editor extends EventEmitter<TLEventMap> {
9671
9787
  // weird but true: what `inputs` calls screen-space is actually viewport space. so
9672
9788
  // we need to convert back into true screen space first. we should fix this...
9673
9789
  Vec.Add(
9674
- this.inputs.currentScreenPoint,
9790
+ this.inputs.getCurrentScreenPoint(),
9675
9791
  this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9676
9792
  ),
9677
9793
  pointerId: options?.pointerId ?? 0,
9678
9794
  button: options?.button ?? 0,
9679
- isPen: options?.isPen ?? this.inputs.isPen,
9680
- shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9681
- altKey: options?.altKey ?? this.inputs.altKey,
9682
- ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9683
- metaKey: options?.metaKey ?? this.inputs.metaKey,
9684
- accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9795
+ isPen: options?.isPen ?? this.inputs.getIsPen(),
9796
+ shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(),
9797
+ altKey: options?.altKey ?? this.inputs.getAltKey(),
9798
+ ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(),
9799
+ metaKey: options?.metaKey ?? this.inputs.getMetaKey(),
9800
+ accelKey: false,
9685
9801
  }
9686
9802
 
9803
+ // needs to be calculated second
9804
+ event.accelKey = options?.accelKey ?? this.inputs.getAccelKey()
9805
+
9687
9806
  if (options?.immediate) {
9688
9807
  this._flushEventForTick(event)
9689
9808
  } else {
@@ -10056,16 +10175,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10056
10175
  /** @internal */
10057
10176
  @bind
10058
10177
  _setShiftKeyTimeout() {
10059
- this.inputs.shiftKey = false
10178
+ this.inputs.setShiftKey(false)
10060
10179
  this.dispatch({
10061
10180
  type: 'keyboard',
10062
10181
  name: 'key_up',
10063
10182
  key: 'Shift',
10064
- shiftKey: this.inputs.shiftKey,
10065
- ctrlKey: this.inputs.ctrlKey,
10066
- altKey: this.inputs.altKey,
10067
- metaKey: this.inputs.metaKey,
10068
- accelKey: isAccelKey(this.inputs),
10183
+ shiftKey: this.inputs.getShiftKey(),
10184
+ ctrlKey: this.inputs.getCtrlKey(),
10185
+ altKey: this.inputs.getAltKey(),
10186
+ metaKey: this.inputs.getMetaKey(),
10187
+ accelKey: this.inputs.getAccelKey(),
10069
10188
  code: 'ShiftLeft',
10070
10189
  })
10071
10190
  }
@@ -10076,16 +10195,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10076
10195
  /** @internal */
10077
10196
  @bind
10078
10197
  _setAltKeyTimeout() {
10079
- this.inputs.altKey = false
10198
+ this.inputs.setAltKey(false)
10080
10199
  this.dispatch({
10081
10200
  type: 'keyboard',
10082
10201
  name: 'key_up',
10083
10202
  key: 'Alt',
10084
- shiftKey: this.inputs.shiftKey,
10085
- ctrlKey: this.inputs.ctrlKey,
10086
- altKey: this.inputs.altKey,
10087
- metaKey: this.inputs.metaKey,
10088
- accelKey: isAccelKey(this.inputs),
10203
+ shiftKey: this.inputs.getShiftKey(),
10204
+ ctrlKey: this.inputs.getCtrlKey(),
10205
+ altKey: this.inputs.getAltKey(),
10206
+ metaKey: this.inputs.getMetaKey(),
10207
+ accelKey: this.inputs.getAccelKey(),
10089
10208
  code: 'AltLeft',
10090
10209
  })
10091
10210
  }
@@ -10096,16 +10215,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10096
10215
  /** @internal */
10097
10216
  @bind
10098
10217
  _setCtrlKeyTimeout() {
10099
- this.inputs.ctrlKey = false
10218
+ this.inputs.setCtrlKey(false)
10100
10219
  this.dispatch({
10101
10220
  type: 'keyboard',
10102
10221
  name: 'key_up',
10103
10222
  key: 'Ctrl',
10104
- shiftKey: this.inputs.shiftKey,
10105
- ctrlKey: this.inputs.ctrlKey,
10106
- altKey: this.inputs.altKey,
10107
- metaKey: this.inputs.metaKey,
10108
- accelKey: isAccelKey(this.inputs),
10223
+ shiftKey: this.inputs.getShiftKey(),
10224
+ ctrlKey: this.inputs.getCtrlKey(),
10225
+ altKey: this.inputs.getAltKey(),
10226
+ metaKey: this.inputs.getMetaKey(),
10227
+ accelKey: this.inputs.getAccelKey(),
10109
10228
  code: 'ControlLeft',
10110
10229
  })
10111
10230
  }
@@ -10116,16 +10235,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10116
10235
  /** @internal */
10117
10236
  @bind
10118
10237
  _setMetaKeyTimeout() {
10119
- this.inputs.metaKey = false
10238
+ this.inputs.setMetaKey(false)
10120
10239
  this.dispatch({
10121
10240
  type: 'keyboard',
10122
10241
  name: 'key_up',
10123
10242
  key: 'Meta',
10124
- shiftKey: this.inputs.shiftKey,
10125
- ctrlKey: this.inputs.ctrlKey,
10126
- altKey: this.inputs.altKey,
10127
- metaKey: this.inputs.metaKey,
10128
- accelKey: isAccelKey(this.inputs),
10243
+ shiftKey: this.inputs.getShiftKey(),
10244
+ ctrlKey: this.inputs.getCtrlKey(),
10245
+ altKey: this.inputs.getAltKey(),
10246
+ metaKey: this.inputs.getMetaKey(),
10247
+ accelKey: this.inputs.getAccelKey(),
10129
10248
  code: 'MetaLeft',
10130
10249
  })
10131
10250
  }
@@ -10133,9 +10252,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10133
10252
  /** @internal */
10134
10253
  private _restoreToolId = 'select'
10135
10254
 
10136
- /** @internal */
10137
- private _pinchStart = 1
10138
-
10139
10255
  /** @internal */
10140
10256
  private _didPinch = false
10141
10257
 
@@ -10242,56 +10358,54 @@ export class Editor extends EventEmitter<TLEventMap> {
10242
10358
  if (info.type === 'misc') {
10243
10359
  // stop panning if the interaction is cancelled or completed
10244
10360
  if (info.name === 'cancel' || info.name === 'complete') {
10245
- this.inputs.isDragging = false
10361
+ this.inputs.setIsDragging(false)
10246
10362
 
10247
- if (this.inputs.isPanning) {
10248
- this.inputs.isPanning = false
10249
- this.inputs.isSpacebarPanning = false
10363
+ if (this.inputs.getIsPanning()) {
10364
+ this.inputs.setIsPanning(false)
10365
+ this.inputs.setIsSpacebarPanning(false)
10250
10366
  this.setCursor({ type: this._prevCursor, rotation: 0 })
10251
10367
  }
10252
10368
  }
10253
10369
 
10254
- this.emit('event', info)
10255
10370
  this.root.handleEvent(info)
10371
+ this.emit('event', info)
10256
10372
  return
10257
10373
  }
10258
10374
 
10259
10375
  if (info.shiftKey) {
10260
10376
  clearTimeout(this._shiftKeyTimeout)
10261
10377
  this._shiftKeyTimeout = -1
10262
- inputs.shiftKey = true
10263
- } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
10378
+ inputs.setShiftKey(true)
10379
+ } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
10264
10380
  this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
10265
10381
  }
10266
10382
 
10267
10383
  if (info.altKey) {
10268
10384
  clearTimeout(this._altKeyTimeout)
10269
10385
  this._altKeyTimeout = -1
10270
- inputs.altKey = true
10271
- } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
10386
+ inputs.setAltKey(true)
10387
+ } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
10272
10388
  this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
10273
10389
  }
10274
10390
 
10275
10391
  if (info.ctrlKey) {
10276
10392
  clearTimeout(this._ctrlKeyTimeout)
10277
10393
  this._ctrlKeyTimeout = -1
10278
- inputs.ctrlKey = true
10279
- } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
10394
+ inputs.setCtrlKey(true)
10395
+ } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
10280
10396
  this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
10281
10397
  }
10282
10398
 
10283
10399
  if (info.metaKey) {
10284
10400
  clearTimeout(this._metaKeyTimeout)
10285
10401
  this._metaKeyTimeout = -1
10286
- inputs.metaKey = true
10287
- } else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
10402
+ inputs.setMetaKey(true)
10403
+ } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
10288
10404
  this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150)
10289
10405
  }
10290
10406
 
10291
- const { originPagePoint, currentPagePoint } = inputs
10292
-
10293
- if (!inputs.isPointing) {
10294
- inputs.isDragging = false
10407
+ if (!inputs.getIsPointing()) {
10408
+ inputs.setIsDragging(false)
10295
10409
  }
10296
10410
 
10297
10411
  const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -10302,29 +10416,29 @@ export class Editor extends EventEmitter<TLEventMap> {
10302
10416
  case 'pinch': {
10303
10417
  if (cameraOptions.isLocked) return
10304
10418
  clearTimeout(this._longPressTimeout)
10305
- this._updateInputsFromEvent(info)
10419
+ this.inputs.updateFromEvent(info)
10306
10420
 
10307
10421
  switch (info.name) {
10308
10422
  case 'pinch_start': {
10309
- if (inputs.isPinching) return
10423
+ if (inputs.getIsPinching()) return
10310
10424
 
10311
- if (!inputs.isEditing) {
10312
- this._pinchStart = this.getCamera().z
10425
+ if (!inputs.getIsEditing()) {
10313
10426
  if (!this._selectedShapeIdsAtPointerDown.length) {
10314
10427
  this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10315
10428
  }
10316
10429
 
10317
10430
  this._didPinch = true
10318
10431
 
10319
- inputs.isPinching = true
10432
+ inputs.setIsPinching(true)
10320
10433
 
10321
10434
  this.interrupt()
10322
10435
  }
10323
10436
 
10437
+ this.emit('event', info)
10324
10438
  return // Stop here!
10325
10439
  }
10326
10440
  case 'pinch': {
10327
- if (!inputs.isPinching) return
10441
+ if (!inputs.getIsPinching()) return
10328
10442
 
10329
10443
  const {
10330
10444
  point: { z = 1 },
@@ -10355,13 +10469,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10355
10469
  { immediate: true }
10356
10470
  )
10357
10471
 
10472
+ this.emit('event', info)
10358
10473
  return // Stop here!
10359
10474
  }
10360
10475
  case 'pinch_end': {
10361
- if (!inputs.isPinching) return this
10476
+ if (!inputs.getIsPinching()) return this
10362
10477
 
10363
10478
  // Stop pinching
10364
- inputs.isPinching = false
10479
+ inputs.setIsPinching(false)
10365
10480
 
10366
10481
  // Stash and clear the shapes that were selected when the pinch started
10367
10482
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
@@ -10381,6 +10496,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10381
10496
  }
10382
10497
  }
10383
10498
 
10499
+ this.emit('event', info)
10384
10500
  return // Stop here!
10385
10501
  }
10386
10502
  }
@@ -10388,7 +10504,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10388
10504
  case 'wheel': {
10389
10505
  if (cameraOptions.isLocked) return
10390
10506
 
10391
- this._updateInputsFromEvent(info)
10507
+ this.inputs.updateFromEvent(info)
10392
10508
 
10393
10509
  const { panSpeed, zoomSpeed } = cameraOptions
10394
10510
  let wheelBehavior = cameraOptions.wheelBehavior
@@ -10419,7 +10535,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10419
10535
  switch (behavior) {
10420
10536
  case 'zoom': {
10421
10537
  // Zoom in on current screen point using the wheel delta
10422
- const { x, y } = this.inputs.currentScreenPoint
10538
+ const { x, y } = this.inputs.getCurrentScreenPoint()
10423
10539
  let delta = dz
10424
10540
 
10425
10541
  // If we're forcing zoom, then we need to do the wheel normalization math here
@@ -10436,6 +10552,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10436
10552
  immediate: true,
10437
10553
  })
10438
10554
  this.maybeTrackPerformance('Zooming')
10555
+ this.root.handleEvent(info)
10556
+ this.emit('event', info)
10439
10557
  return
10440
10558
  }
10441
10559
  case 'pan': {
@@ -10444,6 +10562,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10444
10562
  immediate: true,
10445
10563
  })
10446
10564
  this.maybeTrackPerformance('Panning')
10565
+ this.root.handleEvent(info)
10566
+ this.emit('event', info)
10447
10567
  return
10448
10568
  }
10449
10569
  }
@@ -10452,9 +10572,9 @@ export class Editor extends EventEmitter<TLEventMap> {
10452
10572
  }
10453
10573
  case 'pointer': {
10454
10574
  // Ignore pointer events while we're pinching
10455
- if (inputs.isPinching) return
10575
+ if (inputs.getIsPinching()) return
10456
10576
 
10457
- this._updateInputsFromEvent(info)
10577
+ this.inputs.updateFromEvent(info)
10458
10578
  const { isPen } = info
10459
10579
  const { isPenMode } = instanceState
10460
10580
 
@@ -10463,7 +10583,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10463
10583
  // If we're in pen mode and the input is not a pen type, then stop here
10464
10584
  if (isPenMode && !isPen) return
10465
10585
 
10466
- if (!this.inputs.isPanning) {
10586
+ if (!this.inputs.getIsPanning()) {
10467
10587
  // Start a long press timeout
10468
10588
  this._longPressTimeout = this.timers.setTimeout(() => {
10469
10589
  const vsb = this.getViewportScreenBounds()
@@ -10473,7 +10593,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10473
10593
  // viewport bounds, and will be again when this event is handled...
10474
10594
  // so we need to counter-adjust from the stored value so that the
10475
10595
  // new value is set correctly.
10476
- point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
10596
+ point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y),
10477
10597
  name: 'long_press',
10478
10598
  })
10479
10599
  }, this.options.longPressDurationMs)
@@ -10490,8 +10610,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10490
10610
  inputs.buttons.add(info.button)
10491
10611
 
10492
10612
  // Start pointing and stop dragging
10493
- inputs.isPointing = true
10494
- inputs.isDragging = false
10613
+ inputs.setIsPointing(true)
10614
+ inputs.setIsDragging(false)
10495
10615
 
10496
10616
  // If pen mode is off but we're not already in pen mode, turn that on
10497
10617
  if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
@@ -10503,16 +10623,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10503
10623
  this.setCurrentTool('eraser')
10504
10624
  } else if (info.button === MIDDLE_MOUSE_BUTTON) {
10505
10625
  // Middle mouse pan activates panning unless we're already panning (with spacebar)
10506
- if (!this.inputs.isPanning) {
10626
+ if (!this.inputs.getIsPanning()) {
10507
10627
  this._prevCursor = this.getInstanceState().cursor.type
10508
10628
  }
10509
- this.inputs.isPanning = true
10629
+ this.inputs.setIsPanning(true)
10510
10630
  clearTimeout(this._longPressTimeout)
10511
10631
  }
10512
10632
 
10513
10633
  // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click
10514
10634
  // Also stop here, we don't want the state chart to receive the event
10515
- if (this.inputs.isPanning) {
10635
+ if (this.inputs.getIsPanning()) {
10516
10636
  this.stopCameraAnimation()
10517
10637
  this.setCursor({ type: 'grabbing', rotation: 0 })
10518
10638
  return this
@@ -10527,9 +10647,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10527
10647
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
10528
10648
 
10529
10649
  // If we've started panning, then clear any long press timeout
10530
- if (this.inputs.isPanning && this.inputs.isPointing) {
10650
+ if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) {
10531
10651
  // Handle spacebar / middle mouse button panning
10532
- const { currentScreenPoint, previousScreenPoint } = this.inputs
10652
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
10653
+ const previousScreenPoint = this.inputs.getPreviousScreenPoint()
10533
10654
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10534
10655
  this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10535
10656
  immediate: true,
@@ -10539,24 +10660,25 @@ export class Editor extends EventEmitter<TLEventMap> {
10539
10660
  }
10540
10661
 
10541
10662
  if (
10542
- inputs.isPointing &&
10543
- !inputs.isDragging &&
10544
- Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() >
10663
+ inputs.getIsPointing() &&
10664
+ !inputs.getIsDragging() &&
10665
+ Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) *
10666
+ this.getZoomLevel() >
10545
10667
  (instanceState.isCoarsePointer
10546
10668
  ? this.options.coarseDragDistanceSquared
10547
10669
  : this.options.dragDistanceSquared) /
10548
10670
  cz
10549
10671
  ) {
10550
10672
  // Start dragging
10551
- inputs.isDragging = true
10673
+ inputs.setIsDragging(true)
10552
10674
  clearTimeout(this._longPressTimeout)
10553
10675
  }
10554
10676
  break
10555
10677
  }
10556
10678
  case 'pointer_up': {
10557
10679
  // Stop dragging / pointing
10558
- inputs.isDragging = false
10559
- inputs.isPointing = false
10680
+ inputs.setIsDragging(false)
10681
+ inputs.setIsPointing(false)
10560
10682
  clearTimeout(this._longPressTimeout)
10561
10683
 
10562
10684
  // Remove the button from the buttons set
@@ -10573,12 +10695,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10573
10695
  info.button = 0
10574
10696
  }
10575
10697
 
10576
- if (inputs.isPanning) {
10698
+ if (inputs.getIsPanning()) {
10577
10699
  if (!inputs.keys.has('Space')) {
10578
- inputs.isPanning = false
10579
- inputs.isSpacebarPanning = false
10700
+ inputs.setIsPanning(false)
10701
+ inputs.setIsSpacebarPanning(false)
10580
10702
  }
10581
- const slideDirection = this.inputs.pointerVelocity
10703
+ const slideDirection = this.inputs.getPointerVelocity()
10582
10704
  const slideSpeed = Math.min(2, slideDirection.len())
10583
10705
 
10584
10706
  switch (info.button) {
@@ -10622,43 +10744,48 @@ export class Editor extends EventEmitter<TLEventMap> {
10622
10744
  // Add the key from the keys set
10623
10745
  inputs.keys.add(info.code)
10624
10746
 
10625
- // If the space key is pressed (but meta / control isn't!) activate panning
10626
- if (info.code === 'Space' && !info.ctrlKey) {
10627
- if (!this.inputs.isPanning) {
10628
- this._prevCursor = instanceState.cursor.type
10629
- }
10747
+ if (this.options.spacebarPanning) {
10748
+ // If the space key is pressed (but meta / control isn't!) activate panning
10749
+ if (info.code === 'Space' && !info.ctrlKey) {
10750
+ if (!this.inputs.getIsPanning()) {
10751
+ this._prevCursor = instanceState.cursor.type
10752
+ }
10630
10753
 
10631
- this.inputs.isPanning = true
10632
- this.inputs.isSpacebarPanning = true
10633
- clearTimeout(this._longPressTimeout)
10634
- this.setCursor({ type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 })
10635
- }
10754
+ this.inputs.setIsPanning(true)
10755
+ this.inputs.setIsSpacebarPanning(true)
10756
+ clearTimeout(this._longPressTimeout)
10757
+ this.setCursor({
10758
+ type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
10759
+ rotation: 0,
10760
+ })
10761
+ }
10636
10762
 
10637
- if (this.inputs.isSpacebarPanning) {
10638
- let offset: Vec | undefined
10639
- switch (info.code) {
10640
- case 'ArrowUp': {
10641
- offset = new Vec(0, -1)
10642
- break
10643
- }
10644
- case 'ArrowRight': {
10645
- offset = new Vec(1, 0)
10646
- break
10647
- }
10648
- case 'ArrowDown': {
10649
- offset = new Vec(0, 1)
10650
- break
10651
- }
10652
- case 'ArrowLeft': {
10653
- offset = new Vec(-1, 0)
10654
- break
10763
+ if (this.inputs.getIsSpacebarPanning()) {
10764
+ let offset: Vec | undefined
10765
+ switch (info.code) {
10766
+ case 'ArrowUp': {
10767
+ offset = new Vec(0, -1)
10768
+ break
10769
+ }
10770
+ case 'ArrowRight': {
10771
+ offset = new Vec(1, 0)
10772
+ break
10773
+ }
10774
+ case 'ArrowDown': {
10775
+ offset = new Vec(0, 1)
10776
+ break
10777
+ }
10778
+ case 'ArrowLeft': {
10779
+ offset = new Vec(-1, 0)
10780
+ break
10781
+ }
10655
10782
  }
10656
- }
10657
10783
 
10658
- if (offset) {
10659
- const bounds = this.getViewportPageBounds()
10660
- const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10661
- this._animateToViewport(next, { animation: { duration: 320 } })
10784
+ if (offset) {
10785
+ const bounds = this.getViewportPageBounds()
10786
+ const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10787
+ this._animateToViewport(next, { animation: { duration: 320 } })
10788
+ }
10662
10789
  }
10663
10790
  }
10664
10791
 
@@ -10668,15 +10795,17 @@ export class Editor extends EventEmitter<TLEventMap> {
10668
10795
  // Remove the key from the keys set
10669
10796
  inputs.keys.delete(info.code)
10670
10797
 
10671
- // If we've lifted the space key,
10672
- if (info.code === 'Space') {
10673
- if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10674
- // If we're still middle dragging, continue panning
10675
- } else {
10676
- // otherwise, stop panning
10677
- this.inputs.isPanning = false
10678
- this.inputs.isSpacebarPanning = false
10679
- this.setCursor({ type: this._prevCursor, rotation: 0 })
10798
+ if (this.options.spacebarPanning) {
10799
+ // If we've lifted the space key,
10800
+ if (info.code === 'Space') {
10801
+ if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10802
+ // If we're still middle dragging, continue panning
10803
+ } else {
10804
+ // otherwise, stop panning
10805
+ this.inputs.setIsPanning(false)
10806
+ this.inputs.setIsSpacebarPanning(false)
10807
+ this.setCursor({ type: this._prevCursor, rotation: 0 })
10808
+ }
10680
10809
  }
10681
10810
  }
10682
10811
  break