@tldraw/editor 4.3.0-canary.c5efe11c58e0 → 4.3.0-canary.cb6779b4f066

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 (178) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +448 -120
  3. package/dist-cjs/index.js +8 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  7. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -5
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/constants.js +1 -3
  11. package/dist-cjs/lib/constants.js.map +2 -2
  12. package/dist-cjs/lib/editor/Editor.js +346 -280
  13. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  14. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -23
  15. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  17. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  19. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  21. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  23. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  25. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  27. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  28. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +181 -0
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  30. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  31. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  33. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  34. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  35. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.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/parseCss.js +1 -1
  40. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  41. package/dist-cjs/lib/globals/environment.js +45 -9
  42. package/dist-cjs/lib/globals/environment.js.map +2 -2
  43. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  44. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  45. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  46. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  48. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  49. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  52. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  54. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  55. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  56. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  57. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  58. package/dist-cjs/lib/options.js +6 -1
  59. package/dist-cjs/lib/options.js.map +2 -2
  60. package/dist-cjs/lib/primitives/Box.js +3 -0
  61. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  62. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  63. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  64. package/dist-cjs/lib/utils/rotation.js +1 -1
  65. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  66. package/dist-cjs/version.js +3 -3
  67. package/dist-cjs/version.js.map +1 -1
  68. package/dist-esm/index.d.mts +448 -120
  69. package/dist-esm/index.mjs +9 -2
  70. package/dist-esm/index.mjs.map +2 -2
  71. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  72. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  73. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  74. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -5
  75. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  76. package/dist-esm/lib/constants.mjs +1 -3
  77. package/dist-esm/lib/constants.mjs.map +2 -2
  78. package/dist-esm/lib/editor/Editor.mjs +347 -283
  79. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  80. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -23
  81. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  82. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  83. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  84. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  85. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  87. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  88. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  89. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  90. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  91. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  92. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  93. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  94. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +161 -0
  95. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  96. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  97. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  98. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  99. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  100. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  101. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  102. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  103. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  104. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  105. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  106. package/dist-esm/lib/globals/environment.mjs +45 -9
  107. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  108. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  109. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  110. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  111. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  112. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  113. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  114. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  115. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  116. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  117. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  118. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  119. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  120. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  121. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  122. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  123. package/dist-esm/lib/options.mjs +6 -1
  124. package/dist-esm/lib/options.mjs.map +2 -2
  125. package/dist-esm/lib/primitives/Box.mjs +3 -0
  126. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  127. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  128. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  129. package/dist-esm/lib/utils/rotation.mjs +1 -1
  130. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  131. package/dist-esm/version.mjs +3 -3
  132. package/dist-esm/version.mjs.map +1 -1
  133. package/editor.css +14 -12
  134. package/package.json +21 -17
  135. package/src/index.ts +5 -1
  136. package/src/lib/components/ErrorBoundary.tsx +1 -1
  137. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  138. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -8
  139. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  140. package/src/lib/constants.ts +0 -2
  141. package/src/lib/editor/Editor.test.ts +140 -0
  142. package/src/lib/editor/Editor.ts +452 -326
  143. package/src/lib/editor/derivations/notVisibleShapes.ts +21 -33
  144. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  145. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  146. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  147. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  148. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  149. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  150. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  151. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  152. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  153. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  154. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +215 -0
  155. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  156. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  157. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  158. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  159. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  160. package/src/lib/editor/types/emit-types.ts +3 -1
  161. package/src/lib/exports/parseCss.test.ts +1 -0
  162. package/src/lib/exports/parseCss.ts +1 -1
  163. package/src/lib/globals/environment.ts +65 -10
  164. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  165. package/src/lib/hooks/useEvent.tsx +1 -1
  166. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  167. package/src/lib/hooks/useGestureEvents.ts +2 -2
  168. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  169. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  170. package/src/lib/hooks/useScreenBounds.ts +1 -1
  171. package/src/lib/hooks/useStateAttribute.ts +4 -1
  172. package/src/lib/hooks/useTransform.ts +1 -1
  173. package/src/lib/hooks/useZoomCss.ts +3 -8
  174. package/src/lib/options.ts +32 -0
  175. package/src/lib/primitives/Box.ts +9 -0
  176. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  177. package/src/lib/utils/rotation.ts +1 -1
  178. 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,28 @@ 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
+ private readonly _spatialIndex: SpatialIndexManager
903
+
893
904
  /**
894
905
  * A manager for the any asynchronous events and making sure they're
895
906
  * cleaned up upon disposal.
@@ -969,6 +980,7 @@ export class Editor extends EventEmitter<TLEventMap> {
969
980
  this.disposables.clear()
970
981
  this.store.dispose()
971
982
  this.isDisposed = true
983
+ this.emit('dispose')
972
984
  }
973
985
 
974
986
  /* ------------------- Shape Utils ------------------ */
@@ -1060,7 +1072,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1060
1072
  /* --------------------- History -------------------- */
1061
1073
 
1062
1074
  /**
1063
- * A manager for the app's history.
1075
+ * A manager for the editor's history.
1064
1076
  *
1065
1077
  * @readonly
1066
1078
  */
@@ -1084,14 +1096,18 @@ export class Editor extends EventEmitter<TLEventMap> {
1084
1096
  }
1085
1097
 
1086
1098
  /**
1087
- * Whether the app can undo.
1099
+ * Whether the editor can undo.
1088
1100
  *
1089
1101
  * @public
1090
1102
  */
1091
- @computed getCanUndo(): boolean {
1103
+ @computed canUndo(): boolean {
1092
1104
  return this.history.getNumUndos() > 0
1093
1105
  }
1094
1106
 
1107
+ getCanUndo() {
1108
+ return this.canUndo()
1109
+ }
1110
+
1095
1111
  /**
1096
1112
  * Redo to the next mark.
1097
1113
  *
@@ -1109,20 +1125,24 @@ export class Editor extends EventEmitter<TLEventMap> {
1109
1125
  return this
1110
1126
  }
1111
1127
 
1112
- clearHistory() {
1113
- this.history.clear()
1114
- return this
1115
- }
1116
-
1117
1128
  /**
1118
- * Whether the app can redo.
1129
+ * Whether the editor can redo.
1119
1130
  *
1120
1131
  * @public
1121
1132
  */
1122
- @computed getCanRedo(): boolean {
1133
+ @computed canRedo(): boolean {
1123
1134
  return this.history.getNumRedos() > 0
1124
1135
  }
1125
1136
 
1137
+ getCanRedo() {
1138
+ return this.canRedo()
1139
+ }
1140
+
1141
+ clearHistory() {
1142
+ this.history.clear()
1143
+ return this
1144
+ }
1145
+
1126
1146
  /**
1127
1147
  * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
1128
1148
  * any redos. You typically want to do this just before a user interaction begins or is handled.
@@ -1296,7 +1316,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1296
1316
  }),
1297
1317
  selectionCount: this.getSelectedShapes().length,
1298
1318
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1299
- inputs: this.inputs,
1319
+ inputs: this.inputs.toJson(),
1300
1320
  pageState: this.getCurrentPageState(),
1301
1321
  instanceState: this.getInstanceState(),
1302
1322
  collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
@@ -1321,7 +1341,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1321
1341
  * we're in a transaction that's about to be rolled back due to the same error we're currently
1322
1342
  * reporting.
1323
1343
  *
1324
- * Instead, to listen to changes to this value, you need to listen to app's `crash` event.
1344
+ * Instead, to listen to changes to this value, you need to listen to editor's `crash` event.
1325
1345
  *
1326
1346
  * @internal
1327
1347
  */
@@ -2024,7 +2044,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2024
2044
  }
2025
2045
 
2026
2046
  /**
2027
- * The id of the app's only selected shape.
2047
+ * The id of the editor's only selected shape.
2028
2048
  *
2029
2049
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
2030
2050
  *
@@ -2036,7 +2056,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2036
2056
  }
2037
2057
 
2038
2058
  /**
2039
- * The app's only selected shape.
2059
+ * The editor's only selected shape.
2040
2060
  *
2041
2061
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
2042
2062
  *
@@ -2277,6 +2297,29 @@ export class Editor extends EventEmitter<TLEventMap> {
2277
2297
  return editingShapeId ? this.getShape(editingShapeId) : undefined
2278
2298
  }
2279
2299
 
2300
+ /**
2301
+ * Whether the shape can be edited.
2302
+ *
2303
+ * @param shape - The shape (or shape id) to check if it can be edited.
2304
+ * @param info - The info about the edit start.
2305
+ *
2306
+ * @public
2307
+ * @returns true if the shape can be edited, false otherwise.
2308
+ */
2309
+ canEditShape<T extends TLShape | TLShapeId>(shape: T | null, info?: TLEditStartInfo): shape is T {
2310
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2311
+ if (!id) return false // no shape
2312
+ if (id === this.getEditingShapeId()) return false // already editing this shape
2313
+ const _shape = this.getShape(id)
2314
+ if (!_shape) return false // no shape
2315
+ const util = this.getShapeUtil(_shape)
2316
+ const _info: TLEditStartInfo = info ?? { type: 'unknown' }
2317
+ if (!util.canEdit(_shape, _info)) return false // shape is not editable
2318
+ if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception
2319
+ if (this.isShapeOrAncestorLocked(_shape) && !util.canEditWhileLocked(_shape)) return false // locked and no exception. Note here: we're not distinguishing between a locked shape and a shape that is the descendant of a locked shape.
2320
+ return true // shape is editable
2321
+ }
2322
+
2280
2323
  /**
2281
2324
  * Set the current editing shape.
2282
2325
  *
@@ -2292,44 +2335,59 @@ export class Editor extends EventEmitter<TLEventMap> {
2292
2335
  */
2293
2336
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2294
2337
  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
2338
 
2318
- // Either we just set the editing id to null, or the shape was missing or not editable
2339
+ if (!id) {
2340
+ // setting the editing shape to null
2319
2341
  this.run(
2320
2342
  () => {
2321
- this._updateCurrentPageState({ editingShapeId: null })
2322
- this._currentRichTextEditor.set(null)
2343
+ // Clean up the previous editing shape
2344
+ const prevEditingShapeId = this.getEditingShapeId()
2323
2345
  if (prevEditingShapeId) {
2324
2346
  const prevEditingShape = this.getShape(prevEditingShapeId)
2325
2347
  if (prevEditingShape) {
2326
2348
  this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2327
2349
  }
2328
2350
  }
2351
+
2352
+ // Clean up the editing shape state and rich text editor
2353
+ this._updateCurrentPageState({ editingShapeId: null })
2354
+ this._currentRichTextEditor.set(null)
2329
2355
  },
2330
2356
  { history: 'ignore' }
2331
2357
  )
2358
+
2359
+ return this
2332
2360
  }
2361
+
2362
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2363
+ if (!this.canEditShape(id)) return this
2364
+
2365
+ // id was provided and the next editing shape is editable, so set the rich text editor to null
2366
+ this.run(
2367
+ () => {
2368
+ // Clean up the previous editing shape
2369
+ const prevEditingShapeId = this.getEditingShapeId()
2370
+ if (prevEditingShapeId) {
2371
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2372
+ if (prevEditingShape) {
2373
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2374
+ }
2375
+ }
2376
+
2377
+ // Clean up the editing shape state and rich text editor
2378
+ this._updateCurrentPageState({ editingShapeId: null })
2379
+ this._currentRichTextEditor.set(null)
2380
+
2381
+ // Set the new editing shape
2382
+ this.select(id)
2383
+ this._updateCurrentPageState({ editingShapeId: id })
2384
+
2385
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2386
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2387
+ },
2388
+ { history: 'ignore' }
2389
+ )
2390
+
2333
2391
  return this
2334
2392
  }
2335
2393
 
@@ -2533,6 +2591,26 @@ export class Editor extends EventEmitter<TLEventMap> {
2533
2591
  return this.getCurrentPageState().croppingShapeId
2534
2592
  }
2535
2593
 
2594
+ /**
2595
+ * Whether the shape can be cropped.
2596
+ *
2597
+ * @param shape - The shape (or shape id) to check if it can be cropped.
2598
+ *
2599
+ * @public
2600
+ * @returns true if the shape can be cropped, false otherwise.
2601
+ */
2602
+ canCropShape<T extends TLShape | TLShapeId>(shape: T | null): shape is T {
2603
+ if (!shape) return false
2604
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2605
+ if (!id) return false
2606
+ const _shape = this.getShape(id)
2607
+ if (!_shape) return false
2608
+ const util = this.getShapeUtil(_shape)
2609
+ if (!util.canCrop(_shape)) return false
2610
+ if (this.isShapeOrAncestorLocked(_shape)) return false
2611
+ return true
2612
+ }
2613
+
2536
2614
  /**
2537
2615
  * Set the current cropping shape.
2538
2616
  *
@@ -2554,12 +2632,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2554
2632
  () => {
2555
2633
  if (!id) {
2556
2634
  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
- }
2635
+ } else if (this.canCropShape(id)) {
2636
+ this.updateCurrentPageState({ croppingShapeId: id })
2563
2637
  }
2564
2638
  },
2565
2639
  { history: 'ignore' }
@@ -2669,6 +2743,52 @@ export class Editor extends EventEmitter<TLEventMap> {
2669
2743
  return this.getCamera().z
2670
2744
  }
2671
2745
 
2746
+ private _debouncedZoomLevel = atom('debounced zoom level', 1)
2747
+
2748
+ /**
2749
+ * Get the debounced zoom level. When the camera is moving, this returns the zoom level
2750
+ * from when the camera started moving rather than the current zoom level. This can be
2751
+ * used to avoid expensive re-renders during camera movements.
2752
+ *
2753
+ * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom`
2754
+ * is `false`, this method always returns the current zoom level.
2755
+ *
2756
+ * @public
2757
+ */
2758
+ @computed getDebouncedZoomLevel() {
2759
+ if (this.options.debouncedZoom) {
2760
+ if (this.getCameraState() === 'idle') {
2761
+ return this.getZoomLevel()
2762
+ } else {
2763
+ return this._debouncedZoomLevel.get()
2764
+ }
2765
+ }
2766
+
2767
+ return this.getZoomLevel()
2768
+ }
2769
+
2770
+ @computed private _getAboveDebouncedZoomThreshold() {
2771
+ return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold
2772
+ }
2773
+
2774
+ /**
2775
+ * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2776
+ * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2777
+ *
2778
+ * @public
2779
+ * @example
2780
+ * ```ts
2781
+ * editor.getEfficientZoomLevel()
2782
+ * ```
2783
+ *
2784
+ * @public
2785
+ */
2786
+ @computed getEfficientZoomLevel() {
2787
+ return this._getAboveDebouncedZoomThreshold()
2788
+ ? this.getDebouncedZoomLevel()
2789
+ : this.getZoomLevel()
2790
+ }
2791
+
2672
2792
  /**
2673
2793
  * Get the camera's initial or reset zoom level.
2674
2794
  *
@@ -2995,7 +3115,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2995
3115
 
2996
3116
  // Dispatch a new pointer move because the pointer's page will have changed
2997
3117
  // (its screen position will compute to a new page position given the new camera position)
2998
- const { currentScreenPoint, currentPagePoint } = this.inputs
3118
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
3119
+ const currentPagePoint = this.inputs.getCurrentPagePoint()
2999
3120
 
3000
3121
  // compare the next page point (derived from the current camera) to the current page point
3001
3122
  if (
@@ -3159,7 +3280,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3159
3280
  * ```ts
3160
3281
  * editor.zoomIn()
3161
3282
  * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
3162
- * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
3283
+ * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
3163
3284
  * ```
3164
3285
  *
3165
3286
  * @param point - The screen point to zoom in on. Defaults to the screen center
@@ -3204,7 +3325,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3204
3325
  * ```ts
3205
3326
  * editor.zoomOut()
3206
3327
  * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
3207
- * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
3328
+ * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } })
3208
3329
  * ```
3209
3330
  *
3210
3331
  * @param point - The point to zoom out on. Defaults to the viewport screen center.
@@ -3261,10 +3382,17 @@ export class Editor extends EventEmitter<TLEventMap> {
3261
3382
 
3262
3383
  const selectionPageBounds = this.getSelectionPageBounds()
3263
3384
  if (selectionPageBounds) {
3264
- this.zoomToBounds(selectionPageBounds, {
3265
- targetZoom: Math.max(1, this.getZoomLevel()),
3266
- ...opts,
3267
- })
3385
+ const currentZoom = this.getZoomLevel()
3386
+ // If already at 100%, zoom to fit the selection in the viewport
3387
+ // Otherwise, zoom to 100% centered on the selection
3388
+ if (Math.abs(currentZoom - 1) < 0.01) {
3389
+ this.zoomToBounds(selectionPageBounds, opts)
3390
+ } else {
3391
+ this.zoomToBounds(selectionPageBounds, {
3392
+ targetZoom: 1,
3393
+ ...opts,
3394
+ })
3395
+ }
3268
3396
  }
3269
3397
  return this
3270
3398
  }
@@ -3321,7 +3449,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3321
3449
 
3322
3450
  const viewportScreenBounds = this.getViewportScreenBounds()
3323
3451
 
3324
- const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28)
3452
+ const inset =
3453
+ opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28)
3325
3454
 
3326
3455
  const baseZoom = this.getBaseZoom()
3327
3456
  const zoomMin = cameraOptions.zoomSteps[0]
@@ -3631,22 +3760,23 @@ export class Editor extends EventEmitter<TLEventMap> {
3631
3760
  if (_willSetInitialBounds) {
3632
3761
  // If we have just received the initial bounds, don't center the camera.
3633
3762
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3763
+ this.emit('resize', screenBounds.toJson())
3634
3764
  this.setCamera(this.getCamera())
3635
3765
  } else {
3636
3766
  if (center && !this.getInstanceState().followingUserId) {
3637
3767
  // Get the page center before the change, make the change, and restore it
3638
3768
  const before = this.getViewportPageBounds().center
3639
3769
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3770
+ this.emit('resize', screenBounds.toJson())
3640
3771
  this.centerOnPoint(before)
3641
3772
  } else {
3642
3773
  // Otherwise,
3643
3774
  this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
3775
+ this.emit('resize', screenBounds.toJson())
3644
3776
  this._setCamera(Vec.From({ ...this.getCamera() }))
3645
3777
  }
3646
3778
  }
3647
3779
 
3648
- this._tickCameraState()
3649
-
3650
3780
  return this
3651
3781
  }
3652
3782
 
@@ -4052,18 +4182,19 @@ export class Editor extends EventEmitter<TLEventMap> {
4052
4182
  // box just for rendering, and we only update after the camera stops moving.
4053
4183
  private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4054
4184
  private _cameraStateTimeoutRemaining = 0
4055
- _decayCameraStateTimeout(elapsed: number) {
4185
+ private _decayCameraStateTimeout(elapsed: number) {
4056
4186
  this._cameraStateTimeoutRemaining -= elapsed
4057
4187
  if (this._cameraStateTimeoutRemaining > 0) return
4058
4188
  this.off('tick', this._decayCameraStateTimeout)
4059
4189
  this._cameraState.set('idle')
4060
4190
  }
4061
- _tickCameraState() {
4191
+ private _tickCameraState() {
4062
4192
  // always reset the timeout
4063
4193
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4064
4194
  // If the state is idle, then start the tick
4065
4195
  if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4066
4196
  this._cameraState.set('moving')
4197
+ this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4067
4198
  this.on('tick', this._decayCameraStateTimeout)
4068
4199
  }
4069
4200
 
@@ -5010,6 +5141,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5010
5141
  }
5011
5142
 
5012
5143
  private _notVisibleShapes = notVisibleShapes(this)
5144
+ private _culledShapesCache: Set<TLShapeId> | null = null
5013
5145
 
5014
5146
  /**
5015
5147
  * Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
@@ -5021,16 +5153,41 @@ export class Editor extends EventEmitter<TLEventMap> {
5021
5153
  const notVisibleShapes = this.getNotVisibleShapes()
5022
5154
  const selectedShapeIds = this.getSelectedShapeIds()
5023
5155
  const editingId = this.getEditingShapeId()
5024
- const culledShapes = new Set<TLShapeId>(notVisibleShapes)
5156
+ const nextValue = new Set<TLShapeId>(notVisibleShapes)
5025
5157
  // we don't cull the shape we are editing
5026
5158
  if (editingId) {
5027
- culledShapes.delete(editingId)
5159
+ nextValue.delete(editingId)
5028
5160
  }
5029
5161
  // we also don't cull selected shapes
5030
5162
  selectedShapeIds.forEach((id) => {
5031
- culledShapes.delete(id)
5163
+ nextValue.delete(id)
5032
5164
  })
5033
- return culledShapes
5165
+
5166
+ // Cache optimization: return same Set object if contents unchanged
5167
+ // This allows consumers to use === comparison and prevents unnecessary re-renders
5168
+ const prevValue = this._culledShapesCache
5169
+ if (prevValue) {
5170
+ // If sizes differ, contents must differ
5171
+ if (prevValue.size !== nextValue.size) {
5172
+ this._culledShapesCache = nextValue
5173
+ return nextValue
5174
+ }
5175
+
5176
+ // Check if all elements are the same
5177
+ for (const id of prevValue) {
5178
+ if (!nextValue.has(id)) {
5179
+ // Found a difference, update cache and return new set
5180
+ this._culledShapesCache = nextValue
5181
+ return nextValue
5182
+ }
5183
+ }
5184
+
5185
+ // Loop completed without finding differences - contents identical
5186
+ return prevValue
5187
+ }
5188
+
5189
+ this._culledShapesCache = nextValue
5190
+ return nextValue
5034
5191
  }
5035
5192
 
5036
5193
  /**
@@ -5097,11 +5254,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5097
5254
  let inMarginClosestToEdgeDistance = Infinity
5098
5255
  let inMarginClosestToEdgeHit: TLShape | null = null
5099
5256
 
5257
+ // Use larger margin for spatial search to account for edge distance checks
5258
+ const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
5259
+ const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
5260
+
5100
5261
  const shapesToCheck = (
5101
5262
  opts.renderingOnly
5102
5263
  ? this.getCurrentPageRenderingShapesSorted()
5103
5264
  : this.getCurrentPageShapesSorted()
5104
5265
  ).filter((shape) => {
5266
+ // Frames have labels positioned above the shape (outside bounds), so always include them
5267
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5268
+
5105
5269
  if (
5106
5270
  (shape.isLocked && !hitLocked) ||
5107
5271
  this.isShapeHidden(shape) ||
@@ -5287,11 +5451,41 @@ export class Editor extends EventEmitter<TLEventMap> {
5287
5451
  point: VecLike,
5288
5452
  opts = {} as { margin?: number; hitInside?: boolean }
5289
5453
  ): TLShape[] {
5454
+ const margin = opts.margin ?? 0
5455
+ const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
5456
+
5457
+ // Get all page shapes in z-index order and filter to candidates that pass isPointInShape
5458
+ // Frames are always checked because their labels can be outside their bounds
5290
5459
  return this.getCurrentPageShapesSorted()
5291
- .filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
5460
+ .filter((shape) => {
5461
+ if (this.isShapeHidden(shape)) return false
5462
+ if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
5463
+ return this.isPointInShape(shape, point, opts)
5464
+ })
5292
5465
  .reverse()
5293
5466
  }
5294
5467
 
5468
+ /**
5469
+ * Get shape IDs within the given bounds.
5470
+ *
5471
+ * Note: Uses shape page bounds only. Frames with labels outside their bounds
5472
+ * may not be included even if the label is within the search bounds.
5473
+ *
5474
+ * Note: Results are unordered. If you need z-order, combine with sorted shapes:
5475
+ * ```ts
5476
+ * const candidates = editor.getShapeIdsInsideBounds(bounds)
5477
+ * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
5478
+ * ```
5479
+ *
5480
+ * @param bounds - The bounds to search within.
5481
+ * @returns Unordered set of shape IDs within the given bounds.
5482
+ *
5483
+ * @internal
5484
+ */
5485
+ getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
5486
+ return this._spatialIndex.getShapeIdsInsideBounds(bounds)
5487
+ }
5488
+
5295
5489
  /**
5296
5490
  * Test whether a point (in the current page space) will will a shape. This method takes into account masks,
5297
5491
  * such as when a shape is the child of a frame and is partially clipped by the frame.
@@ -7649,8 +7843,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7649
7843
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
7650
7844
  // to make sure the shape is mirrored correctly
7651
7845
  if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
7652
- let { rotation } = Mat.Decompose(options.initialPageTransform)
7653
- rotation -= 2 * rotation
7846
+ // We need to compute the new local rotation that will result in the negated page rotation.
7847
+ // For a shape with local rotation `localRot` and parent page rotation `parentRot`:
7848
+ // - pageRot = parentRot + localRot
7849
+ // - newPageRot = -pageRot (we want to negate the page rotation)
7850
+ // - newPageRot = parentRot + newLocalRot (parent hasn't changed)
7851
+ // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot
7852
+ const parentRotation = this.getShapeParentTransform(id).rotation()
7853
+ const rotation = -options.initialShape.rotation - 2 * parentRotation
7654
7854
  this.updateShapes([{ id, type, rotation }])
7655
7855
  }
7656
7856
 
@@ -7670,9 +7870,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7670
7870
  )
7671
7871
 
7672
7872
  // now calculate how far away the shape is from where it needs to be
7673
- const pageBounds = this.getShapePageBounds(id)!
7674
7873
  const pageTransform = this.getShapePageTransform(id)!
7675
- const currentPageCenter = pageBounds.center
7874
+ // We need to use the local bounds center transformed to page space, not the axis-aligned
7875
+ // page bounds center. This is because the page bounds are axis-aligned and their center
7876
+ // changes when the rotation changes, but we want to use the same reference point as
7877
+ // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform).
7878
+ const currentLocalBounds = this.getShapeGeometry(id).bounds
7879
+ const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
7676
7880
  const shapePageTransformOrigin = pageTransform.point()
7677
7881
  if (!currentPageCenter || !shapePageTransformOrigin) return this
7678
7882
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
@@ -8120,7 +8324,12 @@ export class Editor extends EventEmitter<TLEventMap> {
8120
8324
  )
8121
8325
  )
8122
8326
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
8123
- const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
8327
+ const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
8328
+ const pageBounds = Box.Common(childBounds)
8329
+
8330
+ if (!pageBounds.isValid()) {
8331
+ throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`)
8332
+ }
8124
8333
 
8125
8334
  const { x, y } = pageBounds.point
8126
8335
 
@@ -9145,6 +9354,30 @@ export class Editor extends EventEmitter<TLEventMap> {
9145
9354
  }
9146
9355
  }
9147
9356
 
9357
+ if (point) {
9358
+ const shapesById = new Map<TLShapeId, TLShape>(shapes.map((shape) => [shape.id, shape]))
9359
+ const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id)))
9360
+ if (rootShapesFromContent.length > 0) {
9361
+ const targetParent = this.getShapeAtPoint(point, {
9362
+ hitInside: true,
9363
+ hitFrameInside: true,
9364
+ hitLocked: true,
9365
+ filter: (shape) => {
9366
+ const util = this.getShapeUtil(shape)
9367
+ if (!util.canReceiveNewChildrenOfType) return false
9368
+ return rootShapesFromContent.every((rootShape) =>
9369
+ util.canReceiveNewChildrenOfType!(shape, rootShape.type)
9370
+ )
9371
+ },
9372
+ })
9373
+
9374
+ // When pasting at a specific point (e.g. paste-at-cursor) prefer the
9375
+ // parent under the pointer so that we don't keep using the original
9376
+ // selection's parent (which can keep shapes clipped inside frames).
9377
+ pasteParentId = targetParent ? targetParent.id : currentPageId
9378
+ }
9379
+ }
9380
+
9148
9381
  let isDuplicating = false
9149
9382
 
9150
9383
  if (!isPageId(pasteParentId)) {
@@ -9482,126 +9715,6 @@ export class Editor extends EventEmitter<TLEventMap> {
9482
9715
 
9483
9716
  /* --------------------- Events --------------------- */
9484
9717
 
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
9718
  /**
9606
9719
  * Dispatch a cancel event.
9607
9720
  *
@@ -9671,19 +9784,22 @@ export class Editor extends EventEmitter<TLEventMap> {
9671
9784
  // weird but true: what `inputs` calls screen-space is actually viewport space. so
9672
9785
  // we need to convert back into true screen space first. we should fix this...
9673
9786
  Vec.Add(
9674
- this.inputs.currentScreenPoint,
9787
+ this.inputs.getCurrentScreenPoint(),
9675
9788
  this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9676
9789
  ),
9677
9790
  pointerId: options?.pointerId ?? 0,
9678
9791
  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),
9792
+ isPen: options?.isPen ?? this.inputs.getIsPen(),
9793
+ shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(),
9794
+ altKey: options?.altKey ?? this.inputs.getAltKey(),
9795
+ ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(),
9796
+ metaKey: options?.metaKey ?? this.inputs.getMetaKey(),
9797
+ accelKey: false,
9685
9798
  }
9686
9799
 
9800
+ // needs to be calculated second
9801
+ event.accelKey = options?.accelKey ?? this.inputs.getAccelKey()
9802
+
9687
9803
  if (options?.immediate) {
9688
9804
  this._flushEventForTick(event)
9689
9805
  } else {
@@ -10056,16 +10172,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10056
10172
  /** @internal */
10057
10173
  @bind
10058
10174
  _setShiftKeyTimeout() {
10059
- this.inputs.shiftKey = false
10175
+ this.inputs.setShiftKey(false)
10060
10176
  this.dispatch({
10061
10177
  type: 'keyboard',
10062
10178
  name: 'key_up',
10063
10179
  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),
10180
+ shiftKey: this.inputs.getShiftKey(),
10181
+ ctrlKey: this.inputs.getCtrlKey(),
10182
+ altKey: this.inputs.getAltKey(),
10183
+ metaKey: this.inputs.getMetaKey(),
10184
+ accelKey: this.inputs.getAccelKey(),
10069
10185
  code: 'ShiftLeft',
10070
10186
  })
10071
10187
  }
@@ -10076,16 +10192,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10076
10192
  /** @internal */
10077
10193
  @bind
10078
10194
  _setAltKeyTimeout() {
10079
- this.inputs.altKey = false
10195
+ this.inputs.setAltKey(false)
10080
10196
  this.dispatch({
10081
10197
  type: 'keyboard',
10082
10198
  name: 'key_up',
10083
10199
  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),
10200
+ shiftKey: this.inputs.getShiftKey(),
10201
+ ctrlKey: this.inputs.getCtrlKey(),
10202
+ altKey: this.inputs.getAltKey(),
10203
+ metaKey: this.inputs.getMetaKey(),
10204
+ accelKey: this.inputs.getAccelKey(),
10089
10205
  code: 'AltLeft',
10090
10206
  })
10091
10207
  }
@@ -10096,16 +10212,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10096
10212
  /** @internal */
10097
10213
  @bind
10098
10214
  _setCtrlKeyTimeout() {
10099
- this.inputs.ctrlKey = false
10215
+ this.inputs.setCtrlKey(false)
10100
10216
  this.dispatch({
10101
10217
  type: 'keyboard',
10102
10218
  name: 'key_up',
10103
10219
  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),
10220
+ shiftKey: this.inputs.getShiftKey(),
10221
+ ctrlKey: this.inputs.getCtrlKey(),
10222
+ altKey: this.inputs.getAltKey(),
10223
+ metaKey: this.inputs.getMetaKey(),
10224
+ accelKey: this.inputs.getAccelKey(),
10109
10225
  code: 'ControlLeft',
10110
10226
  })
10111
10227
  }
@@ -10116,16 +10232,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10116
10232
  /** @internal */
10117
10233
  @bind
10118
10234
  _setMetaKeyTimeout() {
10119
- this.inputs.metaKey = false
10235
+ this.inputs.setMetaKey(false)
10120
10236
  this.dispatch({
10121
10237
  type: 'keyboard',
10122
10238
  name: 'key_up',
10123
10239
  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),
10240
+ shiftKey: this.inputs.getShiftKey(),
10241
+ ctrlKey: this.inputs.getCtrlKey(),
10242
+ altKey: this.inputs.getAltKey(),
10243
+ metaKey: this.inputs.getMetaKey(),
10244
+ accelKey: this.inputs.getAccelKey(),
10129
10245
  code: 'MetaLeft',
10130
10246
  })
10131
10247
  }
@@ -10133,9 +10249,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10133
10249
  /** @internal */
10134
10250
  private _restoreToolId = 'select'
10135
10251
 
10136
- /** @internal */
10137
- private _pinchStart = 1
10138
-
10139
10252
  /** @internal */
10140
10253
  private _didPinch = false
10141
10254
 
@@ -10242,56 +10355,54 @@ export class Editor extends EventEmitter<TLEventMap> {
10242
10355
  if (info.type === 'misc') {
10243
10356
  // stop panning if the interaction is cancelled or completed
10244
10357
  if (info.name === 'cancel' || info.name === 'complete') {
10245
- this.inputs.isDragging = false
10358
+ this.inputs.setIsDragging(false)
10246
10359
 
10247
- if (this.inputs.isPanning) {
10248
- this.inputs.isPanning = false
10249
- this.inputs.isSpacebarPanning = false
10360
+ if (this.inputs.getIsPanning()) {
10361
+ this.inputs.setIsPanning(false)
10362
+ this.inputs.setIsSpacebarPanning(false)
10250
10363
  this.setCursor({ type: this._prevCursor, rotation: 0 })
10251
10364
  }
10252
10365
  }
10253
10366
 
10254
- this.emit('event', info)
10255
10367
  this.root.handleEvent(info)
10368
+ this.emit('event', info)
10256
10369
  return
10257
10370
  }
10258
10371
 
10259
10372
  if (info.shiftKey) {
10260
10373
  clearTimeout(this._shiftKeyTimeout)
10261
10374
  this._shiftKeyTimeout = -1
10262
- inputs.shiftKey = true
10263
- } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
10375
+ inputs.setShiftKey(true)
10376
+ } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
10264
10377
  this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
10265
10378
  }
10266
10379
 
10267
10380
  if (info.altKey) {
10268
10381
  clearTimeout(this._altKeyTimeout)
10269
10382
  this._altKeyTimeout = -1
10270
- inputs.altKey = true
10271
- } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
10383
+ inputs.setAltKey(true)
10384
+ } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
10272
10385
  this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
10273
10386
  }
10274
10387
 
10275
10388
  if (info.ctrlKey) {
10276
10389
  clearTimeout(this._ctrlKeyTimeout)
10277
10390
  this._ctrlKeyTimeout = -1
10278
- inputs.ctrlKey = true
10279
- } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
10391
+ inputs.setCtrlKey(true)
10392
+ } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
10280
10393
  this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
10281
10394
  }
10282
10395
 
10283
10396
  if (info.metaKey) {
10284
10397
  clearTimeout(this._metaKeyTimeout)
10285
10398
  this._metaKeyTimeout = -1
10286
- inputs.metaKey = true
10287
- } else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
10399
+ inputs.setMetaKey(true)
10400
+ } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
10288
10401
  this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150)
10289
10402
  }
10290
10403
 
10291
- const { originPagePoint, currentPagePoint } = inputs
10292
-
10293
- if (!inputs.isPointing) {
10294
- inputs.isDragging = false
10404
+ if (!inputs.getIsPointing()) {
10405
+ inputs.setIsDragging(false)
10295
10406
  }
10296
10407
 
10297
10408
  const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -10302,29 +10413,29 @@ export class Editor extends EventEmitter<TLEventMap> {
10302
10413
  case 'pinch': {
10303
10414
  if (cameraOptions.isLocked) return
10304
10415
  clearTimeout(this._longPressTimeout)
10305
- this._updateInputsFromEvent(info)
10416
+ this.inputs.updateFromEvent(info)
10306
10417
 
10307
10418
  switch (info.name) {
10308
10419
  case 'pinch_start': {
10309
- if (inputs.isPinching) return
10420
+ if (inputs.getIsPinching()) return
10310
10421
 
10311
- if (!inputs.isEditing) {
10312
- this._pinchStart = this.getCamera().z
10422
+ if (!inputs.getIsEditing()) {
10313
10423
  if (!this._selectedShapeIdsAtPointerDown.length) {
10314
10424
  this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10315
10425
  }
10316
10426
 
10317
10427
  this._didPinch = true
10318
10428
 
10319
- inputs.isPinching = true
10429
+ inputs.setIsPinching(true)
10320
10430
 
10321
10431
  this.interrupt()
10322
10432
  }
10323
10433
 
10434
+ this.emit('event', info)
10324
10435
  return // Stop here!
10325
10436
  }
10326
10437
  case 'pinch': {
10327
- if (!inputs.isPinching) return
10438
+ if (!inputs.getIsPinching()) return
10328
10439
 
10329
10440
  const {
10330
10441
  point: { z = 1 },
@@ -10355,13 +10466,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10355
10466
  { immediate: true }
10356
10467
  )
10357
10468
 
10469
+ this.emit('event', info)
10358
10470
  return // Stop here!
10359
10471
  }
10360
10472
  case 'pinch_end': {
10361
- if (!inputs.isPinching) return this
10473
+ if (!inputs.getIsPinching()) return this
10362
10474
 
10363
10475
  // Stop pinching
10364
- inputs.isPinching = false
10476
+ inputs.setIsPinching(false)
10365
10477
 
10366
10478
  // Stash and clear the shapes that were selected when the pinch started
10367
10479
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
@@ -10381,6 +10493,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10381
10493
  }
10382
10494
  }
10383
10495
 
10496
+ this.emit('event', info)
10384
10497
  return // Stop here!
10385
10498
  }
10386
10499
  }
@@ -10388,7 +10501,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10388
10501
  case 'wheel': {
10389
10502
  if (cameraOptions.isLocked) return
10390
10503
 
10391
- this._updateInputsFromEvent(info)
10504
+ this.inputs.updateFromEvent(info)
10392
10505
 
10393
10506
  const { panSpeed, zoomSpeed } = cameraOptions
10394
10507
  let wheelBehavior = cameraOptions.wheelBehavior
@@ -10419,7 +10532,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10419
10532
  switch (behavior) {
10420
10533
  case 'zoom': {
10421
10534
  // Zoom in on current screen point using the wheel delta
10422
- const { x, y } = this.inputs.currentScreenPoint
10535
+ const { x, y } = this.inputs.getCurrentScreenPoint()
10423
10536
  let delta = dz
10424
10537
 
10425
10538
  // If we're forcing zoom, then we need to do the wheel normalization math here
@@ -10436,6 +10549,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10436
10549
  immediate: true,
10437
10550
  })
10438
10551
  this.maybeTrackPerformance('Zooming')
10552
+ this.root.handleEvent(info)
10553
+ this.emit('event', info)
10439
10554
  return
10440
10555
  }
10441
10556
  case 'pan': {
@@ -10444,6 +10559,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10444
10559
  immediate: true,
10445
10560
  })
10446
10561
  this.maybeTrackPerformance('Panning')
10562
+ this.root.handleEvent(info)
10563
+ this.emit('event', info)
10447
10564
  return
10448
10565
  }
10449
10566
  }
@@ -10452,9 +10569,9 @@ export class Editor extends EventEmitter<TLEventMap> {
10452
10569
  }
10453
10570
  case 'pointer': {
10454
10571
  // Ignore pointer events while we're pinching
10455
- if (inputs.isPinching) return
10572
+ if (inputs.getIsPinching()) return
10456
10573
 
10457
- this._updateInputsFromEvent(info)
10574
+ this.inputs.updateFromEvent(info)
10458
10575
  const { isPen } = info
10459
10576
  const { isPenMode } = instanceState
10460
10577
 
@@ -10463,7 +10580,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10463
10580
  // If we're in pen mode and the input is not a pen type, then stop here
10464
10581
  if (isPenMode && !isPen) return
10465
10582
 
10466
- if (!this.inputs.isPanning) {
10583
+ if (!this.inputs.getIsPanning()) {
10467
10584
  // Start a long press timeout
10468
10585
  this._longPressTimeout = this.timers.setTimeout(() => {
10469
10586
  const vsb = this.getViewportScreenBounds()
@@ -10473,7 +10590,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10473
10590
  // viewport bounds, and will be again when this event is handled...
10474
10591
  // so we need to counter-adjust from the stored value so that the
10475
10592
  // new value is set correctly.
10476
- point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
10593
+ point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y),
10477
10594
  name: 'long_press',
10478
10595
  })
10479
10596
  }, this.options.longPressDurationMs)
@@ -10490,8 +10607,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10490
10607
  inputs.buttons.add(info.button)
10491
10608
 
10492
10609
  // Start pointing and stop dragging
10493
- inputs.isPointing = true
10494
- inputs.isDragging = false
10610
+ inputs.setIsPointing(true)
10611
+ inputs.setIsDragging(false)
10495
10612
 
10496
10613
  // If pen mode is off but we're not already in pen mode, turn that on
10497
10614
  if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
@@ -10503,16 +10620,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10503
10620
  this.setCurrentTool('eraser')
10504
10621
  } else if (info.button === MIDDLE_MOUSE_BUTTON) {
10505
10622
  // Middle mouse pan activates panning unless we're already panning (with spacebar)
10506
- if (!this.inputs.isPanning) {
10623
+ if (!this.inputs.getIsPanning()) {
10507
10624
  this._prevCursor = this.getInstanceState().cursor.type
10508
10625
  }
10509
- this.inputs.isPanning = true
10626
+ this.inputs.setIsPanning(true)
10510
10627
  clearTimeout(this._longPressTimeout)
10511
10628
  }
10512
10629
 
10513
10630
  // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click
10514
10631
  // Also stop here, we don't want the state chart to receive the event
10515
- if (this.inputs.isPanning) {
10632
+ if (this.inputs.getIsPanning()) {
10516
10633
  this.stopCameraAnimation()
10517
10634
  this.setCursor({ type: 'grabbing', rotation: 0 })
10518
10635
  return this
@@ -10527,9 +10644,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10527
10644
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
10528
10645
 
10529
10646
  // If we've started panning, then clear any long press timeout
10530
- if (this.inputs.isPanning && this.inputs.isPointing) {
10647
+ if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) {
10531
10648
  // Handle spacebar / middle mouse button panning
10532
- const { currentScreenPoint, previousScreenPoint } = this.inputs
10649
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
10650
+ const previousScreenPoint = this.inputs.getPreviousScreenPoint()
10533
10651
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10534
10652
  this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10535
10653
  immediate: true,
@@ -10539,24 +10657,25 @@ export class Editor extends EventEmitter<TLEventMap> {
10539
10657
  }
10540
10658
 
10541
10659
  if (
10542
- inputs.isPointing &&
10543
- !inputs.isDragging &&
10544
- Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() >
10660
+ inputs.getIsPointing() &&
10661
+ !inputs.getIsDragging() &&
10662
+ Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) *
10663
+ this.getZoomLevel() >
10545
10664
  (instanceState.isCoarsePointer
10546
10665
  ? this.options.coarseDragDistanceSquared
10547
10666
  : this.options.dragDistanceSquared) /
10548
10667
  cz
10549
10668
  ) {
10550
10669
  // Start dragging
10551
- inputs.isDragging = true
10670
+ inputs.setIsDragging(true)
10552
10671
  clearTimeout(this._longPressTimeout)
10553
10672
  }
10554
10673
  break
10555
10674
  }
10556
10675
  case 'pointer_up': {
10557
10676
  // Stop dragging / pointing
10558
- inputs.isDragging = false
10559
- inputs.isPointing = false
10677
+ inputs.setIsDragging(false)
10678
+ inputs.setIsPointing(false)
10560
10679
  clearTimeout(this._longPressTimeout)
10561
10680
 
10562
10681
  // Remove the button from the buttons set
@@ -10573,12 +10692,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10573
10692
  info.button = 0
10574
10693
  }
10575
10694
 
10576
- if (inputs.isPanning) {
10695
+ if (inputs.getIsPanning()) {
10577
10696
  if (!inputs.keys.has('Space')) {
10578
- inputs.isPanning = false
10579
- inputs.isSpacebarPanning = false
10697
+ inputs.setIsPanning(false)
10698
+ inputs.setIsSpacebarPanning(false)
10580
10699
  }
10581
- const slideDirection = this.inputs.pointerVelocity
10700
+ const slideDirection = this.inputs.getPointerVelocity()
10582
10701
  const slideSpeed = Math.min(2, slideDirection.len())
10583
10702
 
10584
10703
  switch (info.button) {
@@ -10622,43 +10741,48 @@ export class Editor extends EventEmitter<TLEventMap> {
10622
10741
  // Add the key from the keys set
10623
10742
  inputs.keys.add(info.code)
10624
10743
 
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
- }
10744
+ if (this.options.spacebarPanning) {
10745
+ // If the space key is pressed (but meta / control isn't!) activate panning
10746
+ if (info.code === 'Space' && !info.ctrlKey) {
10747
+ if (!this.inputs.getIsPanning()) {
10748
+ this._prevCursor = instanceState.cursor.type
10749
+ }
10630
10750
 
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
- }
10751
+ this.inputs.setIsPanning(true)
10752
+ this.inputs.setIsSpacebarPanning(true)
10753
+ clearTimeout(this._longPressTimeout)
10754
+ this.setCursor({
10755
+ type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
10756
+ rotation: 0,
10757
+ })
10758
+ }
10636
10759
 
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
10760
+ if (this.inputs.getIsSpacebarPanning()) {
10761
+ let offset: Vec | undefined
10762
+ switch (info.code) {
10763
+ case 'ArrowUp': {
10764
+ offset = new Vec(0, -1)
10765
+ break
10766
+ }
10767
+ case 'ArrowRight': {
10768
+ offset = new Vec(1, 0)
10769
+ break
10770
+ }
10771
+ case 'ArrowDown': {
10772
+ offset = new Vec(0, 1)
10773
+ break
10774
+ }
10775
+ case 'ArrowLeft': {
10776
+ offset = new Vec(-1, 0)
10777
+ break
10778
+ }
10655
10779
  }
10656
- }
10657
10780
 
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 } })
10781
+ if (offset) {
10782
+ const bounds = this.getViewportPageBounds()
10783
+ const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10784
+ this._animateToViewport(next, { animation: { duration: 320 } })
10785
+ }
10662
10786
  }
10663
10787
  }
10664
10788
 
@@ -10668,15 +10792,17 @@ export class Editor extends EventEmitter<TLEventMap> {
10668
10792
  // Remove the key from the keys set
10669
10793
  inputs.keys.delete(info.code)
10670
10794
 
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 })
10795
+ if (this.options.spacebarPanning) {
10796
+ // If we've lifted the space key,
10797
+ if (info.code === 'Space') {
10798
+ if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10799
+ // If we're still middle dragging, continue panning
10800
+ } else {
10801
+ // otherwise, stop panning
10802
+ this.inputs.setIsPanning(false)
10803
+ this.inputs.setIsSpacebarPanning(false)
10804
+ this.setCursor({ type: this._prevCursor, rotation: 0 })
10805
+ }
10680
10806
  }
10681
10807
  }
10682
10808
  break