@tldraw/editor 4.3.0-canary.9c474ef3fad5 → 4.3.0-canary.a2419250444e

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 (140) hide show
  1. package/dist-cjs/index.d.ts +387 -118
  2. package/dist-cjs/index.js +5 -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/constants.js +1 -3
  8. package/dist-cjs/lib/constants.js.map +2 -2
  9. package/dist-cjs/lib/editor/Editor.js +247 -273
  10. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +18 -17
  12. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  13. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  14. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  15. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  16. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  18. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  20. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  21. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  22. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  24. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  27. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  28. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  29. package/dist-cjs/lib/exports/parseCss.js +1 -1
  30. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  31. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  32. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  33. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  35. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  36. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  38. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  40. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  41. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  42. package/dist-cjs/lib/options.js +4 -1
  43. package/dist-cjs/lib/options.js.map +2 -2
  44. package/dist-cjs/lib/primitives/Box.js +3 -0
  45. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  47. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  48. package/dist-cjs/lib/utils/rotation.js +1 -1
  49. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  50. package/dist-cjs/version.js +3 -3
  51. package/dist-cjs/version.js.map +1 -1
  52. package/dist-esm/index.d.mts +387 -118
  53. package/dist-esm/index.mjs +5 -1
  54. package/dist-esm/index.mjs.map +2 -2
  55. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  56. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  57. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  58. package/dist-esm/lib/constants.mjs +1 -3
  59. package/dist-esm/lib/constants.mjs.map +2 -2
  60. package/dist-esm/lib/editor/Editor.mjs +248 -276
  61. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  62. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +18 -17
  63. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  64. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  65. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  66. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  67. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  69. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  71. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  72. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  73. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  74. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  75. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  77. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  78. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  79. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  80. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  81. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  83. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  85. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  86. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  87. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  89. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  90. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  91. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  92. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  93. package/dist-esm/lib/options.mjs +4 -1
  94. package/dist-esm/lib/options.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/Box.mjs +3 -0
  96. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  98. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  99. package/dist-esm/lib/utils/rotation.mjs +1 -1
  100. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  101. package/dist-esm/version.mjs +3 -3
  102. package/dist-esm/version.mjs.map +1 -1
  103. package/editor.css +6 -8
  104. package/package.json +18 -16
  105. package/src/index.ts +3 -0
  106. package/src/lib/components/ErrorBoundary.tsx +1 -1
  107. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  108. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  109. package/src/lib/constants.ts +0 -2
  110. package/src/lib/editor/Editor.test.ts +140 -0
  111. package/src/lib/editor/Editor.ts +300 -316
  112. package/src/lib/editor/derivations/notVisibleShapes.ts +37 -23
  113. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  114. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  115. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  116. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  117. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  118. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  119. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  120. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  121. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  122. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  123. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  124. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  125. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  126. package/src/lib/exports/parseCss.test.ts +1 -0
  127. package/src/lib/exports/parseCss.ts +1 -1
  128. package/src/lib/hooks/useEvent.tsx +1 -1
  129. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  130. package/src/lib/hooks/useGestureEvents.ts +2 -2
  131. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  132. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  133. package/src/lib/hooks/useScreenBounds.ts +1 -1
  134. package/src/lib/hooks/useStateAttribute.ts +4 -1
  135. package/src/lib/hooks/useTransform.ts +1 -1
  136. package/src/lib/options.ts +19 -0
  137. package/src/lib/primitives/Box.ts +9 -0
  138. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  139. package/src/lib/utils/rotation.ts +1 -1
  140. 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,18 @@ 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'
154
152
  import { TextManager } from './managers/TextManager/TextManager'
155
153
  import { TickManager } from './managers/TickManager/TickManager'
156
154
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
157
- import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
155
+ import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
158
156
  import { RootState } from './tools/RootState'
159
157
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
160
158
  import { TLContent } from './types/clipboard-types'
161
159
  import { TLEventMap } from './types/emit-types'
162
- import {
163
- TLEventInfo,
164
- TLPinchEventInfo,
165
- TLPointerEventInfo,
166
- TLWheelEventInfo,
167
- } from './types/event-types'
160
+ import { TLEventInfo, TLPointerEventInfo } from './types/event-types'
168
161
  import { TLExternalAsset, TLExternalContent } from './types/external-content'
169
162
  import { TLHistoryBatchOptions } from './types/history-types'
170
163
  import {
@@ -195,7 +188,7 @@ export type TLResizeShapeOptions = Partial<{
195
188
  /** @public */
196
189
  export interface TLEditorOptions {
197
190
  /**
198
- * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading
191
+ * The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading
199
192
  * from a server or database.
200
193
  */
201
194
  store: TLStore
@@ -333,6 +326,8 @@ export class Editor extends EventEmitter<TLEventMap> {
333
326
 
334
327
  this._tickManager = new TickManager(this)
335
328
 
329
+ this.inputs = new InputsManager(this)
330
+
336
331
  class NewRoot extends RootState {
337
332
  static override initial = initialState ?? ''
338
333
  }
@@ -867,7 +862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
867
862
  }
868
863
 
869
864
  /**
870
- * A set of functions to call when the app is disposed.
865
+ * A set of functions to call when the editor is disposed.
871
866
  *
872
867
  * @public
873
868
  */
@@ -880,11 +875,21 @@ export class Editor extends EventEmitter<TLEventMap> {
880
875
  */
881
876
  isDisposed = false
882
877
 
883
- /** @internal */
884
- private readonly _tickManager
878
+ /**
879
+ * A manager for the editor's tick events.
880
+ *
881
+ * @internal */
882
+ private readonly _tickManager: TickManager
885
883
 
886
884
  /**
887
- * A manager for the app's snapping feature.
885
+ * A manager for the editor's input state.
886
+ *
887
+ * @public
888
+ */
889
+ readonly inputs: InputsManager
890
+
891
+ /**
892
+ * A manager for the editor's snapping feature.
888
893
  *
889
894
  * @public
890
895
  */
@@ -1061,7 +1066,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1061
1066
  /* --------------------- History -------------------- */
1062
1067
 
1063
1068
  /**
1064
- * A manager for the app's history.
1069
+ * A manager for the editor's history.
1065
1070
  *
1066
1071
  * @readonly
1067
1072
  */
@@ -1085,14 +1090,18 @@ export class Editor extends EventEmitter<TLEventMap> {
1085
1090
  }
1086
1091
 
1087
1092
  /**
1088
- * Whether the app can undo.
1093
+ * Whether the editor can undo.
1089
1094
  *
1090
1095
  * @public
1091
1096
  */
1092
- @computed getCanUndo(): boolean {
1097
+ @computed canUndo(): boolean {
1093
1098
  return this.history.getNumUndos() > 0
1094
1099
  }
1095
1100
 
1101
+ getCanUndo() {
1102
+ return this.canUndo()
1103
+ }
1104
+
1096
1105
  /**
1097
1106
  * Redo to the next mark.
1098
1107
  *
@@ -1110,20 +1119,24 @@ export class Editor extends EventEmitter<TLEventMap> {
1110
1119
  return this
1111
1120
  }
1112
1121
 
1113
- clearHistory() {
1114
- this.history.clear()
1115
- return this
1116
- }
1117
-
1118
1122
  /**
1119
- * Whether the app can redo.
1123
+ * Whether the editor can redo.
1120
1124
  *
1121
1125
  * @public
1122
1126
  */
1123
- @computed getCanRedo(): boolean {
1127
+ @computed canRedo(): boolean {
1124
1128
  return this.history.getNumRedos() > 0
1125
1129
  }
1126
1130
 
1131
+ getCanRedo() {
1132
+ return this.canRedo()
1133
+ }
1134
+
1135
+ clearHistory() {
1136
+ this.history.clear()
1137
+ return this
1138
+ }
1139
+
1127
1140
  /**
1128
1141
  * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
1129
1142
  * any redos. You typically want to do this just before a user interaction begins or is handled.
@@ -1297,7 +1310,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1297
1310
  }),
1298
1311
  selectionCount: this.getSelectedShapes().length,
1299
1312
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1300
- inputs: this.inputs,
1313
+ inputs: this.inputs.toJson(),
1301
1314
  pageState: this.getCurrentPageState(),
1302
1315
  instanceState: this.getInstanceState(),
1303
1316
  collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
@@ -1322,7 +1335,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1322
1335
  * we're in a transaction that's about to be rolled back due to the same error we're currently
1323
1336
  * reporting.
1324
1337
  *
1325
- * Instead, to listen to changes to this value, you need to listen to app's `crash` event.
1338
+ * Instead, to listen to changes to this value, you need to listen to editor's `crash` event.
1326
1339
  *
1327
1340
  * @internal
1328
1341
  */
@@ -2025,7 +2038,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2025
2038
  }
2026
2039
 
2027
2040
  /**
2028
- * The id of the app's only selected shape.
2041
+ * The id of the editor's only selected shape.
2029
2042
  *
2030
2043
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
2031
2044
  *
@@ -2037,7 +2050,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2037
2050
  }
2038
2051
 
2039
2052
  /**
2040
- * The app's only selected shape.
2053
+ * The editor's only selected shape.
2041
2054
  *
2042
2055
  * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
2043
2056
  *
@@ -2278,6 +2291,29 @@ export class Editor extends EventEmitter<TLEventMap> {
2278
2291
  return editingShapeId ? this.getShape(editingShapeId) : undefined
2279
2292
  }
2280
2293
 
2294
+ /**
2295
+ * Whether the shape can be edited.
2296
+ *
2297
+ * @param shape - The shape (or shape id) to check if it can be edited.
2298
+ * @param info - The info about the edit start.
2299
+ *
2300
+ * @public
2301
+ * @returns true if the shape can be edited, false otherwise.
2302
+ */
2303
+ canEditShape<T extends TLShape | TLShapeId>(shape: T | null, info?: TLEditStartInfo): shape is T {
2304
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2305
+ if (!id) return false // no shape
2306
+ if (id === this.getEditingShapeId()) return false // already editing this shape
2307
+ const _shape = this.getShape(id)
2308
+ if (!_shape) return false // no shape
2309
+ const util = this.getShapeUtil(_shape)
2310
+ const _info: TLEditStartInfo = info ?? { type: 'unknown' }
2311
+ if (!util.canEdit(_shape, _info)) return false // shape is not editable
2312
+ if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception
2313
+ if (this.isShapeOrAncestorLocked(_shape) && !util.canEditWhileLocked(_shape)) return false // locked and no exception. Note here: we're not distinguishing between a locked shape and a shape that is the descendant of a locked shape.
2314
+ return true // shape is editable
2315
+ }
2316
+
2281
2317
  /**
2282
2318
  * Set the current editing shape.
2283
2319
  *
@@ -2293,44 +2329,59 @@ export class Editor extends EventEmitter<TLEventMap> {
2293
2329
  */
2294
2330
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2295
2331
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2296
- this.setRichTextEditor(null)
2297
- const prevEditingShapeId = this.getEditingShapeId()
2298
- if (id !== prevEditingShapeId) {
2299
- if (id) {
2300
- const shape = this.getShape(id)
2301
- if (shape && this.getShapeUtil(shape).canEdit(shape)) {
2302
- this.run(
2303
- () => {
2304
- this._updateCurrentPageState({ editingShapeId: id })
2305
- if (prevEditingShapeId) {
2306
- const prevEditingShape = this.getShape(prevEditingShapeId)
2307
- if (prevEditingShape) {
2308
- this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2309
- }
2310
- }
2311
- this.getShapeUtil(shape).onEditStart?.(shape)
2312
- },
2313
- { history: 'ignore' }
2314
- )
2315
- return this
2316
- }
2317
- }
2318
2332
 
2319
- // Either we just set the editing id to null, or the shape was missing or not editable
2333
+ if (!id) {
2334
+ // setting the editing shape to null
2320
2335
  this.run(
2321
2336
  () => {
2322
- this._updateCurrentPageState({ editingShapeId: null })
2323
- this._currentRichTextEditor.set(null)
2337
+ // Clean up the previous editing shape
2338
+ const prevEditingShapeId = this.getEditingShapeId()
2324
2339
  if (prevEditingShapeId) {
2325
2340
  const prevEditingShape = this.getShape(prevEditingShapeId)
2326
2341
  if (prevEditingShape) {
2327
2342
  this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2328
2343
  }
2329
2344
  }
2345
+
2346
+ // Clean up the editing shape state and rich text editor
2347
+ this._updateCurrentPageState({ editingShapeId: null })
2348
+ this._currentRichTextEditor.set(null)
2330
2349
  },
2331
2350
  { history: 'ignore' }
2332
2351
  )
2352
+
2353
+ return this
2333
2354
  }
2355
+
2356
+ // id was provided but the next editing shape was not editable or didn't exist, so do nothing
2357
+ if (!this.canEditShape(id)) return this
2358
+
2359
+ // id was provided and the next editing shape is editable, so set the rich text editor to null
2360
+ this.run(
2361
+ () => {
2362
+ // Clean up the previous editing shape
2363
+ const prevEditingShapeId = this.getEditingShapeId()
2364
+ if (prevEditingShapeId) {
2365
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2366
+ if (prevEditingShape) {
2367
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2368
+ }
2369
+ }
2370
+
2371
+ // Clean up the editing shape state and rich text editor
2372
+ this._updateCurrentPageState({ editingShapeId: null })
2373
+ this._currentRichTextEditor.set(null)
2374
+
2375
+ // Set the new editing shape
2376
+ this.select(id)
2377
+ this._updateCurrentPageState({ editingShapeId: id })
2378
+
2379
+ const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
2380
+ this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
2381
+ },
2382
+ { history: 'ignore' }
2383
+ )
2384
+
2334
2385
  return this
2335
2386
  }
2336
2387
 
@@ -2534,6 +2585,26 @@ export class Editor extends EventEmitter<TLEventMap> {
2534
2585
  return this.getCurrentPageState().croppingShapeId
2535
2586
  }
2536
2587
 
2588
+ /**
2589
+ * Whether the shape can be cropped.
2590
+ *
2591
+ * @param shape - The shape (or shape id) to check if it can be cropped.
2592
+ *
2593
+ * @public
2594
+ * @returns true if the shape can be cropped, false otherwise.
2595
+ */
2596
+ canCropShape<T extends TLShape | TLShapeId>(shape: T | null): shape is T {
2597
+ if (!shape) return false
2598
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2599
+ if (!id) return false
2600
+ const _shape = this.getShape(id)
2601
+ if (!_shape) return false
2602
+ const util = this.getShapeUtil(_shape)
2603
+ if (!util.canCrop(_shape)) return false
2604
+ if (this.isShapeOrAncestorLocked(_shape)) return false
2605
+ return true
2606
+ }
2607
+
2537
2608
  /**
2538
2609
  * Set the current cropping shape.
2539
2610
  *
@@ -2555,12 +2626,8 @@ export class Editor extends EventEmitter<TLEventMap> {
2555
2626
  () => {
2556
2627
  if (!id) {
2557
2628
  this.updateCurrentPageState({ croppingShapeId: null })
2558
- } else {
2559
- const shape = this.getShape(id)!
2560
- const util = this.getShapeUtil(shape)
2561
- if (shape && util.canCrop(shape)) {
2562
- this.updateCurrentPageState({ croppingShapeId: id })
2563
- }
2629
+ } else if (this.canCropShape(id)) {
2630
+ this.updateCurrentPageState({ croppingShapeId: id })
2564
2631
  }
2565
2632
  },
2566
2633
  { history: 'ignore' }
@@ -3042,7 +3109,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3042
3109
 
3043
3110
  // Dispatch a new pointer move because the pointer's page will have changed
3044
3111
  // (its screen position will compute to a new page position given the new camera position)
3045
- const { currentScreenPoint, currentPagePoint } = this.inputs
3112
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
3113
+ const currentPagePoint = this.inputs.getCurrentPagePoint()
3046
3114
 
3047
3115
  // compare the next page point (derived from the current camera) to the current page point
3048
3116
  if (
@@ -3206,7 +3274,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3206
3274
  * ```ts
3207
3275
  * editor.zoomIn()
3208
3276
  * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })
3209
- * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })
3277
+ * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } })
3210
3278
  * ```
3211
3279
  *
3212
3280
  * @param point - The screen point to zoom in on. Defaults to the screen center
@@ -3251,7 +3319,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3251
3319
  * ```ts
3252
3320
  * editor.zoomOut()
3253
3321
  * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })
3254
- * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })
3322
+ * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } })
3255
3323
  * ```
3256
3324
  *
3257
3325
  * @param point - The point to zoom out on. Defaults to the viewport screen center.
@@ -3308,10 +3376,17 @@ export class Editor extends EventEmitter<TLEventMap> {
3308
3376
 
3309
3377
  const selectionPageBounds = this.getSelectionPageBounds()
3310
3378
  if (selectionPageBounds) {
3311
- this.zoomToBounds(selectionPageBounds, {
3312
- targetZoom: Math.max(1, this.getZoomLevel()),
3313
- ...opts,
3314
- })
3379
+ const currentZoom = this.getZoomLevel()
3380
+ // If already at 100%, zoom to fit the selection in the viewport
3381
+ // Otherwise, zoom to 100% centered on the selection
3382
+ if (Math.abs(currentZoom - 1) < 0.01) {
3383
+ this.zoomToBounds(selectionPageBounds, opts)
3384
+ } else {
3385
+ this.zoomToBounds(selectionPageBounds, {
3386
+ targetZoom: 1,
3387
+ ...opts,
3388
+ })
3389
+ }
3315
3390
  }
3316
3391
  return this
3317
3392
  }
@@ -3368,7 +3443,8 @@ export class Editor extends EventEmitter<TLEventMap> {
3368
3443
 
3369
3444
  const viewportScreenBounds = this.getViewportScreenBounds()
3370
3445
 
3371
- const inset = opts?.inset ?? Math.min(ZOOM_TO_FIT_PADDING, viewportScreenBounds.width * 0.28)
3446
+ const inset =
3447
+ opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28)
3372
3448
 
3373
3449
  const baseZoom = this.getBaseZoom()
3374
3450
  const zoomMin = cameraOptions.zoomSteps[0]
@@ -7698,8 +7774,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7698
7774
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
7699
7775
  // to make sure the shape is mirrored correctly
7700
7776
  if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
7701
- let { rotation } = Mat.Decompose(options.initialPageTransform)
7702
- rotation -= 2 * rotation
7777
+ // We need to compute the new local rotation that will result in the negated page rotation.
7778
+ // For a shape with local rotation `localRot` and parent page rotation `parentRot`:
7779
+ // - pageRot = parentRot + localRot
7780
+ // - newPageRot = -pageRot (we want to negate the page rotation)
7781
+ // - newPageRot = parentRot + newLocalRot (parent hasn't changed)
7782
+ // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot
7783
+ const parentRotation = this.getShapeParentTransform(id).rotation()
7784
+ const rotation = -options.initialShape.rotation - 2 * parentRotation
7703
7785
  this.updateShapes([{ id, type, rotation }])
7704
7786
  }
7705
7787
 
@@ -7719,9 +7801,13 @@ export class Editor extends EventEmitter<TLEventMap> {
7719
7801
  )
7720
7802
 
7721
7803
  // now calculate how far away the shape is from where it needs to be
7722
- const pageBounds = this.getShapePageBounds(id)!
7723
7804
  const pageTransform = this.getShapePageTransform(id)!
7724
- const currentPageCenter = pageBounds.center
7805
+ // We need to use the local bounds center transformed to page space, not the axis-aligned
7806
+ // page bounds center. This is because the page bounds are axis-aligned and their center
7807
+ // changes when the rotation changes, but we want to use the same reference point as
7808
+ // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform).
7809
+ const currentLocalBounds = this.getShapeGeometry(id).bounds
7810
+ const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
7725
7811
  const shapePageTransformOrigin = pageTransform.point()
7726
7812
  if (!currentPageCenter || !shapePageTransformOrigin) return this
7727
7813
  const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
@@ -8169,7 +8255,12 @@ export class Editor extends EventEmitter<TLEventMap> {
8169
8255
  )
8170
8256
  )
8171
8257
  const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
8172
- const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
8258
+ const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
8259
+ const pageBounds = Box.Common(childBounds)
8260
+
8261
+ if (!pageBounds.isValid()) {
8262
+ throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`)
8263
+ }
8173
8264
 
8174
8265
  const { x, y } = pageBounds.point
8175
8266
 
@@ -9555,126 +9646,6 @@ export class Editor extends EventEmitter<TLEventMap> {
9555
9646
 
9556
9647
  /* --------------------- Events --------------------- */
9557
9648
 
9558
- /**
9559
- * The app's current input state.
9560
- *
9561
- * @public
9562
- */
9563
- inputs = {
9564
- /** The most recent pointer down's position in the current page space. */
9565
- originPagePoint: new Vec(),
9566
- /** The most recent pointer down's position in screen space. */
9567
- originScreenPoint: new Vec(),
9568
- /** The previous pointer position in the current page space. */
9569
- previousPagePoint: new Vec(),
9570
- /** The previous pointer position in screen space. */
9571
- previousScreenPoint: new Vec(),
9572
- /** The most recent pointer position in the current page space. */
9573
- currentPagePoint: new Vec(),
9574
- /** The most recent pointer position in screen space. */
9575
- currentScreenPoint: new Vec(),
9576
- /** A set containing the currently pressed keys. */
9577
- keys: new Set<string>(),
9578
- /** A set containing the currently pressed buttons. */
9579
- buttons: new Set<number>(),
9580
- /** Whether the input is from a pe. */
9581
- isPen: false,
9582
- /** Whether the shift key is currently pressed. */
9583
- shiftKey: false,
9584
- /** Whether the meta key is currently pressed. */
9585
- metaKey: false,
9586
- /** Whether the control or command key is currently pressed. */
9587
- ctrlKey: false,
9588
- /** Whether the alt or option key is currently pressed. */
9589
- altKey: false,
9590
- /** Whether the user is dragging. */
9591
- isDragging: false,
9592
- /** Whether the user is pointing. */
9593
- isPointing: false,
9594
- /** Whether the user is pinching. */
9595
- isPinching: false,
9596
- /** Whether the user is editing. */
9597
- isEditing: false,
9598
- /** Whether the user is panning. */
9599
- isPanning: false,
9600
- /** Whether the user is spacebar panning. */
9601
- isSpacebarPanning: false,
9602
- /** Velocity of mouse pointer, in pixels per millisecond */
9603
- pointerVelocity: new Vec(),
9604
- }
9605
-
9606
- /**
9607
- * Update the input points from a pointer, pinch, or wheel event.
9608
- *
9609
- * @param info - The event info.
9610
- */
9611
- private _updateInputsFromEvent(
9612
- info: TLPointerEventInfo | TLPinchEventInfo | TLWheelEventInfo
9613
- ): void {
9614
- const {
9615
- pointerVelocity,
9616
- previousScreenPoint,
9617
- previousPagePoint,
9618
- currentScreenPoint,
9619
- currentPagePoint,
9620
- originScreenPoint,
9621
- originPagePoint,
9622
- } = this.inputs
9623
-
9624
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
9625
- const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
9626
-
9627
- const sx = info.point.x - screenBounds.x
9628
- const sy = info.point.y - screenBounds.y
9629
- const sz = info.point.z ?? 0.5
9630
-
9631
- previousScreenPoint.setTo(currentScreenPoint)
9632
- previousPagePoint.setTo(currentPagePoint)
9633
-
9634
- // The "screen bounds" is relative to the user's actual screen.
9635
- // The "screen point" is relative to the "screen bounds";
9636
- // it will be 0,0 when its actual screen position is equal
9637
- // to screenBounds.point. This is confusing!
9638
- currentScreenPoint.set(sx, sy)
9639
- const nx = sx / cz - cx
9640
- const ny = sy / cz - cy
9641
- if (isFinite(nx) && isFinite(ny)) {
9642
- currentPagePoint.set(nx, ny, sz)
9643
- }
9644
-
9645
- this.inputs.isPen = info.type === 'pointer' && info.isPen
9646
-
9647
- // Reset velocity on pointer down, or when a pinch starts or ends
9648
- if (info.name === 'pointer_down' || this.inputs.isPinching) {
9649
- pointerVelocity.set(0, 0)
9650
- originScreenPoint.setTo(currentScreenPoint)
9651
- originPagePoint.setTo(currentPagePoint)
9652
- }
9653
-
9654
- // todo: We only have to do this if there are multiple users in the document
9655
- this.run(
9656
- () => {
9657
- this.store.put([
9658
- {
9659
- id: TLPOINTER_ID,
9660
- typeName: 'pointer',
9661
- x: currentPagePoint.x,
9662
- y: currentPagePoint.y,
9663
- lastActivityTimestamp:
9664
- // If our pointer moved only because we're following some other user, then don't
9665
- // update our last activity timestamp; otherwise, update it to the current timestamp.
9666
- info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
9667
- ? (this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
9668
- this._tickManager.now)
9669
- : this._tickManager.now,
9670
- meta: {},
9671
- },
9672
- ])
9673
- },
9674
- { history: 'ignore' }
9675
- )
9676
- }
9677
-
9678
9649
  /**
9679
9650
  * Dispatch a cancel event.
9680
9651
  *
@@ -9744,19 +9715,22 @@ export class Editor extends EventEmitter<TLEventMap> {
9744
9715
  // weird but true: what `inputs` calls screen-space is actually viewport space. so
9745
9716
  // we need to convert back into true screen space first. we should fix this...
9746
9717
  Vec.Add(
9747
- this.inputs.currentScreenPoint,
9718
+ this.inputs.getCurrentScreenPoint(),
9748
9719
  this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9749
9720
  ),
9750
9721
  pointerId: options?.pointerId ?? 0,
9751
9722
  button: options?.button ?? 0,
9752
- isPen: options?.isPen ?? this.inputs.isPen,
9753
- shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9754
- altKey: options?.altKey ?? this.inputs.altKey,
9755
- ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9756
- metaKey: options?.metaKey ?? this.inputs.metaKey,
9757
- accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9723
+ isPen: options?.isPen ?? this.inputs.getIsPen(),
9724
+ shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(),
9725
+ altKey: options?.altKey ?? this.inputs.getAltKey(),
9726
+ ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(),
9727
+ metaKey: options?.metaKey ?? this.inputs.getMetaKey(),
9728
+ accelKey: false,
9758
9729
  }
9759
9730
 
9731
+ // needs to be calculated second
9732
+ event.accelKey = options?.accelKey ?? this.inputs.getAccelKey()
9733
+
9760
9734
  if (options?.immediate) {
9761
9735
  this._flushEventForTick(event)
9762
9736
  } else {
@@ -10129,16 +10103,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10129
10103
  /** @internal */
10130
10104
  @bind
10131
10105
  _setShiftKeyTimeout() {
10132
- this.inputs.shiftKey = false
10106
+ this.inputs.setShiftKey(false)
10133
10107
  this.dispatch({
10134
10108
  type: 'keyboard',
10135
10109
  name: 'key_up',
10136
10110
  key: 'Shift',
10137
- shiftKey: this.inputs.shiftKey,
10138
- ctrlKey: this.inputs.ctrlKey,
10139
- altKey: this.inputs.altKey,
10140
- metaKey: this.inputs.metaKey,
10141
- accelKey: isAccelKey(this.inputs),
10111
+ shiftKey: this.inputs.getShiftKey(),
10112
+ ctrlKey: this.inputs.getCtrlKey(),
10113
+ altKey: this.inputs.getAltKey(),
10114
+ metaKey: this.inputs.getMetaKey(),
10115
+ accelKey: this.inputs.getAccelKey(),
10142
10116
  code: 'ShiftLeft',
10143
10117
  })
10144
10118
  }
@@ -10149,16 +10123,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10149
10123
  /** @internal */
10150
10124
  @bind
10151
10125
  _setAltKeyTimeout() {
10152
- this.inputs.altKey = false
10126
+ this.inputs.setAltKey(false)
10153
10127
  this.dispatch({
10154
10128
  type: 'keyboard',
10155
10129
  name: 'key_up',
10156
10130
  key: 'Alt',
10157
- shiftKey: this.inputs.shiftKey,
10158
- ctrlKey: this.inputs.ctrlKey,
10159
- altKey: this.inputs.altKey,
10160
- metaKey: this.inputs.metaKey,
10161
- accelKey: isAccelKey(this.inputs),
10131
+ shiftKey: this.inputs.getShiftKey(),
10132
+ ctrlKey: this.inputs.getCtrlKey(),
10133
+ altKey: this.inputs.getAltKey(),
10134
+ metaKey: this.inputs.getMetaKey(),
10135
+ accelKey: this.inputs.getAccelKey(),
10162
10136
  code: 'AltLeft',
10163
10137
  })
10164
10138
  }
@@ -10169,16 +10143,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10169
10143
  /** @internal */
10170
10144
  @bind
10171
10145
  _setCtrlKeyTimeout() {
10172
- this.inputs.ctrlKey = false
10146
+ this.inputs.setCtrlKey(false)
10173
10147
  this.dispatch({
10174
10148
  type: 'keyboard',
10175
10149
  name: 'key_up',
10176
10150
  key: 'Ctrl',
10177
- shiftKey: this.inputs.shiftKey,
10178
- ctrlKey: this.inputs.ctrlKey,
10179
- altKey: this.inputs.altKey,
10180
- metaKey: this.inputs.metaKey,
10181
- accelKey: isAccelKey(this.inputs),
10151
+ shiftKey: this.inputs.getShiftKey(),
10152
+ ctrlKey: this.inputs.getCtrlKey(),
10153
+ altKey: this.inputs.getAltKey(),
10154
+ metaKey: this.inputs.getMetaKey(),
10155
+ accelKey: this.inputs.getAccelKey(),
10182
10156
  code: 'ControlLeft',
10183
10157
  })
10184
10158
  }
@@ -10189,16 +10163,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10189
10163
  /** @internal */
10190
10164
  @bind
10191
10165
  _setMetaKeyTimeout() {
10192
- this.inputs.metaKey = false
10166
+ this.inputs.setMetaKey(false)
10193
10167
  this.dispatch({
10194
10168
  type: 'keyboard',
10195
10169
  name: 'key_up',
10196
10170
  key: 'Meta',
10197
- shiftKey: this.inputs.shiftKey,
10198
- ctrlKey: this.inputs.ctrlKey,
10199
- altKey: this.inputs.altKey,
10200
- metaKey: this.inputs.metaKey,
10201
- accelKey: isAccelKey(this.inputs),
10171
+ shiftKey: this.inputs.getShiftKey(),
10172
+ ctrlKey: this.inputs.getCtrlKey(),
10173
+ altKey: this.inputs.getAltKey(),
10174
+ metaKey: this.inputs.getMetaKey(),
10175
+ accelKey: this.inputs.getAccelKey(),
10202
10176
  code: 'MetaLeft',
10203
10177
  })
10204
10178
  }
@@ -10206,9 +10180,6 @@ export class Editor extends EventEmitter<TLEventMap> {
10206
10180
  /** @internal */
10207
10181
  private _restoreToolId = 'select'
10208
10182
 
10209
- /** @internal */
10210
- private _pinchStart = 1
10211
-
10212
10183
  /** @internal */
10213
10184
  private _didPinch = false
10214
10185
 
@@ -10315,11 +10286,11 @@ export class Editor extends EventEmitter<TLEventMap> {
10315
10286
  if (info.type === 'misc') {
10316
10287
  // stop panning if the interaction is cancelled or completed
10317
10288
  if (info.name === 'cancel' || info.name === 'complete') {
10318
- this.inputs.isDragging = false
10289
+ this.inputs.setIsDragging(false)
10319
10290
 
10320
- if (this.inputs.isPanning) {
10321
- this.inputs.isPanning = false
10322
- this.inputs.isSpacebarPanning = false
10291
+ if (this.inputs.getIsPanning()) {
10292
+ this.inputs.setIsPanning(false)
10293
+ this.inputs.setIsSpacebarPanning(false)
10323
10294
  this.setCursor({ type: this._prevCursor, rotation: 0 })
10324
10295
  }
10325
10296
  }
@@ -10332,39 +10303,37 @@ export class Editor extends EventEmitter<TLEventMap> {
10332
10303
  if (info.shiftKey) {
10333
10304
  clearTimeout(this._shiftKeyTimeout)
10334
10305
  this._shiftKeyTimeout = -1
10335
- inputs.shiftKey = true
10336
- } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
10306
+ inputs.setShiftKey(true)
10307
+ } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
10337
10308
  this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
10338
10309
  }
10339
10310
 
10340
10311
  if (info.altKey) {
10341
10312
  clearTimeout(this._altKeyTimeout)
10342
10313
  this._altKeyTimeout = -1
10343
- inputs.altKey = true
10344
- } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
10314
+ inputs.setAltKey(true)
10315
+ } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
10345
10316
  this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
10346
10317
  }
10347
10318
 
10348
10319
  if (info.ctrlKey) {
10349
10320
  clearTimeout(this._ctrlKeyTimeout)
10350
10321
  this._ctrlKeyTimeout = -1
10351
- inputs.ctrlKey = true
10352
- } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
10322
+ inputs.setCtrlKey(true)
10323
+ } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
10353
10324
  this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
10354
10325
  }
10355
10326
 
10356
10327
  if (info.metaKey) {
10357
10328
  clearTimeout(this._metaKeyTimeout)
10358
10329
  this._metaKeyTimeout = -1
10359
- inputs.metaKey = true
10360
- } else if (!info.metaKey && inputs.metaKey && this._metaKeyTimeout === -1) {
10330
+ inputs.setMetaKey(true)
10331
+ } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
10361
10332
  this._metaKeyTimeout = this.timers.setTimeout(this._setMetaKeyTimeout, 150)
10362
10333
  }
10363
10334
 
10364
- const { originPagePoint, currentPagePoint } = inputs
10365
-
10366
- if (!inputs.isPointing) {
10367
- inputs.isDragging = false
10335
+ if (!inputs.getIsPointing()) {
10336
+ inputs.setIsDragging(false)
10368
10337
  }
10369
10338
 
10370
10339
  const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -10375,29 +10344,29 @@ export class Editor extends EventEmitter<TLEventMap> {
10375
10344
  case 'pinch': {
10376
10345
  if (cameraOptions.isLocked) return
10377
10346
  clearTimeout(this._longPressTimeout)
10378
- this._updateInputsFromEvent(info)
10347
+ this.inputs.updateFromEvent(info)
10379
10348
 
10380
10349
  switch (info.name) {
10381
10350
  case 'pinch_start': {
10382
- if (inputs.isPinching) return
10351
+ if (inputs.getIsPinching()) return
10383
10352
 
10384
- if (!inputs.isEditing) {
10385
- this._pinchStart = this.getCamera().z
10353
+ if (!inputs.getIsEditing()) {
10386
10354
  if (!this._selectedShapeIdsAtPointerDown.length) {
10387
10355
  this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10388
10356
  }
10389
10357
 
10390
10358
  this._didPinch = true
10391
10359
 
10392
- inputs.isPinching = true
10360
+ inputs.setIsPinching(true)
10393
10361
 
10394
10362
  this.interrupt()
10395
10363
  }
10396
10364
 
10365
+ this.emit('event', info)
10397
10366
  return // Stop here!
10398
10367
  }
10399
10368
  case 'pinch': {
10400
- if (!inputs.isPinching) return
10369
+ if (!inputs.getIsPinching()) return
10401
10370
 
10402
10371
  const {
10403
10372
  point: { z = 1 },
@@ -10428,13 +10397,14 @@ export class Editor extends EventEmitter<TLEventMap> {
10428
10397
  { immediate: true }
10429
10398
  )
10430
10399
 
10400
+ this.emit('event', info)
10431
10401
  return // Stop here!
10432
10402
  }
10433
10403
  case 'pinch_end': {
10434
- if (!inputs.isPinching) return this
10404
+ if (!inputs.getIsPinching()) return this
10435
10405
 
10436
10406
  // Stop pinching
10437
- inputs.isPinching = false
10407
+ inputs.setIsPinching(false)
10438
10408
 
10439
10409
  // Stash and clear the shapes that were selected when the pinch started
10440
10410
  const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this
@@ -10454,6 +10424,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10454
10424
  }
10455
10425
  }
10456
10426
 
10427
+ this.emit('event', info)
10457
10428
  return // Stop here!
10458
10429
  }
10459
10430
  }
@@ -10461,7 +10432,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10461
10432
  case 'wheel': {
10462
10433
  if (cameraOptions.isLocked) return
10463
10434
 
10464
- this._updateInputsFromEvent(info)
10435
+ this.inputs.updateFromEvent(info)
10465
10436
 
10466
10437
  const { panSpeed, zoomSpeed } = cameraOptions
10467
10438
  let wheelBehavior = cameraOptions.wheelBehavior
@@ -10492,7 +10463,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10492
10463
  switch (behavior) {
10493
10464
  case 'zoom': {
10494
10465
  // Zoom in on current screen point using the wheel delta
10495
- const { x, y } = this.inputs.currentScreenPoint
10466
+ const { x, y } = this.inputs.getCurrentScreenPoint()
10496
10467
  let delta = dz
10497
10468
 
10498
10469
  // If we're forcing zoom, then we need to do the wheel normalization math here
@@ -10509,6 +10480,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10509
10480
  immediate: true,
10510
10481
  })
10511
10482
  this.maybeTrackPerformance('Zooming')
10483
+ this.root.handleEvent(info)
10484
+ this.emit('event', info)
10512
10485
  return
10513
10486
  }
10514
10487
  case 'pan': {
@@ -10517,6 +10490,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10517
10490
  immediate: true,
10518
10491
  })
10519
10492
  this.maybeTrackPerformance('Panning')
10493
+ this.root.handleEvent(info)
10494
+ this.emit('event', info)
10520
10495
  return
10521
10496
  }
10522
10497
  }
@@ -10525,9 +10500,9 @@ export class Editor extends EventEmitter<TLEventMap> {
10525
10500
  }
10526
10501
  case 'pointer': {
10527
10502
  // Ignore pointer events while we're pinching
10528
- if (inputs.isPinching) return
10503
+ if (inputs.getIsPinching()) return
10529
10504
 
10530
- this._updateInputsFromEvent(info)
10505
+ this.inputs.updateFromEvent(info)
10531
10506
  const { isPen } = info
10532
10507
  const { isPenMode } = instanceState
10533
10508
 
@@ -10536,7 +10511,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10536
10511
  // If we're in pen mode and the input is not a pen type, then stop here
10537
10512
  if (isPenMode && !isPen) return
10538
10513
 
10539
- if (!this.inputs.isPanning) {
10514
+ if (!this.inputs.getIsPanning()) {
10540
10515
  // Start a long press timeout
10541
10516
  this._longPressTimeout = this.timers.setTimeout(() => {
10542
10517
  const vsb = this.getViewportScreenBounds()
@@ -10546,7 +10521,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10546
10521
  // viewport bounds, and will be again when this event is handled...
10547
10522
  // so we need to counter-adjust from the stored value so that the
10548
10523
  // new value is set correctly.
10549
- point: this.inputs.originScreenPoint.clone().addXY(vsb.x, vsb.y),
10524
+ point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y),
10550
10525
  name: 'long_press',
10551
10526
  })
10552
10527
  }, this.options.longPressDurationMs)
@@ -10563,8 +10538,8 @@ export class Editor extends EventEmitter<TLEventMap> {
10563
10538
  inputs.buttons.add(info.button)
10564
10539
 
10565
10540
  // Start pointing and stop dragging
10566
- inputs.isPointing = true
10567
- inputs.isDragging = false
10541
+ inputs.setIsPointing(true)
10542
+ inputs.setIsDragging(false)
10568
10543
 
10569
10544
  // If pen mode is off but we're not already in pen mode, turn that on
10570
10545
  if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true })
@@ -10576,16 +10551,16 @@ export class Editor extends EventEmitter<TLEventMap> {
10576
10551
  this.setCurrentTool('eraser')
10577
10552
  } else if (info.button === MIDDLE_MOUSE_BUTTON) {
10578
10553
  // Middle mouse pan activates panning unless we're already panning (with spacebar)
10579
- if (!this.inputs.isPanning) {
10554
+ if (!this.inputs.getIsPanning()) {
10580
10555
  this._prevCursor = this.getInstanceState().cursor.type
10581
10556
  }
10582
- this.inputs.isPanning = true
10557
+ this.inputs.setIsPanning(true)
10583
10558
  clearTimeout(this._longPressTimeout)
10584
10559
  }
10585
10560
 
10586
10561
  // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click
10587
10562
  // Also stop here, we don't want the state chart to receive the event
10588
- if (this.inputs.isPanning) {
10563
+ if (this.inputs.getIsPanning()) {
10589
10564
  this.stopCameraAnimation()
10590
10565
  this.setCursor({ type: 'grabbing', rotation: 0 })
10591
10566
  return this
@@ -10600,9 +10575,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10600
10575
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
10601
10576
 
10602
10577
  // If we've started panning, then clear any long press timeout
10603
- if (this.inputs.isPanning && this.inputs.isPointing) {
10578
+ if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) {
10604
10579
  // Handle spacebar / middle mouse button panning
10605
- const { currentScreenPoint, previousScreenPoint } = this.inputs
10580
+ const currentScreenPoint = this.inputs.getCurrentScreenPoint()
10581
+ const previousScreenPoint = this.inputs.getPreviousScreenPoint()
10606
10582
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10607
10583
  this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10608
10584
  immediate: true,
@@ -10612,24 +10588,25 @@ export class Editor extends EventEmitter<TLEventMap> {
10612
10588
  }
10613
10589
 
10614
10590
  if (
10615
- inputs.isPointing &&
10616
- !inputs.isDragging &&
10617
- Vec.Dist2(originPagePoint, currentPagePoint) * this.getZoomLevel() >
10591
+ inputs.getIsPointing() &&
10592
+ !inputs.getIsDragging() &&
10593
+ Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) *
10594
+ this.getZoomLevel() >
10618
10595
  (instanceState.isCoarsePointer
10619
10596
  ? this.options.coarseDragDistanceSquared
10620
10597
  : this.options.dragDistanceSquared) /
10621
10598
  cz
10622
10599
  ) {
10623
10600
  // Start dragging
10624
- inputs.isDragging = true
10601
+ inputs.setIsDragging(true)
10625
10602
  clearTimeout(this._longPressTimeout)
10626
10603
  }
10627
10604
  break
10628
10605
  }
10629
10606
  case 'pointer_up': {
10630
10607
  // Stop dragging / pointing
10631
- inputs.isDragging = false
10632
- inputs.isPointing = false
10608
+ inputs.setIsDragging(false)
10609
+ inputs.setIsPointing(false)
10633
10610
  clearTimeout(this._longPressTimeout)
10634
10611
 
10635
10612
  // Remove the button from the buttons set
@@ -10646,12 +10623,12 @@ export class Editor extends EventEmitter<TLEventMap> {
10646
10623
  info.button = 0
10647
10624
  }
10648
10625
 
10649
- if (inputs.isPanning) {
10626
+ if (inputs.getIsPanning()) {
10650
10627
  if (!inputs.keys.has('Space')) {
10651
- inputs.isPanning = false
10652
- inputs.isSpacebarPanning = false
10628
+ inputs.setIsPanning(false)
10629
+ inputs.setIsSpacebarPanning(false)
10653
10630
  }
10654
- const slideDirection = this.inputs.pointerVelocity
10631
+ const slideDirection = this.inputs.getPointerVelocity()
10655
10632
  const slideSpeed = Math.min(2, slideDirection.len())
10656
10633
 
10657
10634
  switch (info.button) {
@@ -10695,43 +10672,48 @@ export class Editor extends EventEmitter<TLEventMap> {
10695
10672
  // Add the key from the keys set
10696
10673
  inputs.keys.add(info.code)
10697
10674
 
10698
- // If the space key is pressed (but meta / control isn't!) activate panning
10699
- if (info.code === 'Space' && !info.ctrlKey) {
10700
- if (!this.inputs.isPanning) {
10701
- this._prevCursor = instanceState.cursor.type
10702
- }
10675
+ if (this.options.spacebarPanning) {
10676
+ // If the space key is pressed (but meta / control isn't!) activate panning
10677
+ if (info.code === 'Space' && !info.ctrlKey) {
10678
+ if (!this.inputs.getIsPanning()) {
10679
+ this._prevCursor = instanceState.cursor.type
10680
+ }
10703
10681
 
10704
- this.inputs.isPanning = true
10705
- this.inputs.isSpacebarPanning = true
10706
- clearTimeout(this._longPressTimeout)
10707
- this.setCursor({ type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 })
10708
- }
10682
+ this.inputs.setIsPanning(true)
10683
+ this.inputs.setIsSpacebarPanning(true)
10684
+ clearTimeout(this._longPressTimeout)
10685
+ this.setCursor({
10686
+ type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
10687
+ rotation: 0,
10688
+ })
10689
+ }
10709
10690
 
10710
- if (this.inputs.isSpacebarPanning) {
10711
- let offset: Vec | undefined
10712
- switch (info.code) {
10713
- case 'ArrowUp': {
10714
- offset = new Vec(0, -1)
10715
- break
10716
- }
10717
- case 'ArrowRight': {
10718
- offset = new Vec(1, 0)
10719
- break
10720
- }
10721
- case 'ArrowDown': {
10722
- offset = new Vec(0, 1)
10723
- break
10724
- }
10725
- case 'ArrowLeft': {
10726
- offset = new Vec(-1, 0)
10727
- break
10691
+ if (this.inputs.getIsSpacebarPanning()) {
10692
+ let offset: Vec | undefined
10693
+ switch (info.code) {
10694
+ case 'ArrowUp': {
10695
+ offset = new Vec(0, -1)
10696
+ break
10697
+ }
10698
+ case 'ArrowRight': {
10699
+ offset = new Vec(1, 0)
10700
+ break
10701
+ }
10702
+ case 'ArrowDown': {
10703
+ offset = new Vec(0, 1)
10704
+ break
10705
+ }
10706
+ case 'ArrowLeft': {
10707
+ offset = new Vec(-1, 0)
10708
+ break
10709
+ }
10728
10710
  }
10729
- }
10730
10711
 
10731
- if (offset) {
10732
- const bounds = this.getViewportPageBounds()
10733
- const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10734
- this._animateToViewport(next, { animation: { duration: 320 } })
10712
+ if (offset) {
10713
+ const bounds = this.getViewportPageBounds()
10714
+ const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
10715
+ this._animateToViewport(next, { animation: { duration: 320 } })
10716
+ }
10735
10717
  }
10736
10718
  }
10737
10719
 
@@ -10741,15 +10723,17 @@ export class Editor extends EventEmitter<TLEventMap> {
10741
10723
  // Remove the key from the keys set
10742
10724
  inputs.keys.delete(info.code)
10743
10725
 
10744
- // If we've lifted the space key,
10745
- if (info.code === 'Space') {
10746
- if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10747
- // If we're still middle dragging, continue panning
10748
- } else {
10749
- // otherwise, stop panning
10750
- this.inputs.isPanning = false
10751
- this.inputs.isSpacebarPanning = false
10752
- this.setCursor({ type: this._prevCursor, rotation: 0 })
10726
+ if (this.options.spacebarPanning) {
10727
+ // If we've lifted the space key,
10728
+ if (info.code === 'Space') {
10729
+ if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) {
10730
+ // If we're still middle dragging, continue panning
10731
+ } else {
10732
+ // otherwise, stop panning
10733
+ this.inputs.setIsPanning(false)
10734
+ this.inputs.setIsSpacebarPanning(false)
10735
+ this.setCursor({ type: this._prevCursor, rotation: 0 })
10736
+ }
10753
10737
  }
10754
10738
  }
10755
10739
  break