@tldraw/editor 3.15.0 → 3.16.0-canary.01f62b6d4455

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 (124) hide show
  1. package/dist-cjs/index.d.ts +185 -9
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +3 -1
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +4 -26
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  12. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  17. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
  19. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
  20. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  21. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  22. package/dist-cjs/lib/editor/Editor.js +110 -59
  23. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
  25. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  27. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  28. package/dist-cjs/lib/exports/getSvgJsx.js +1 -2
  29. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -20
  31. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  32. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  33. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useStateAttribute.js +35 -0
  35. package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
  36. package/dist-cjs/lib/license/Watermark.js +6 -6
  37. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  38. package/dist-cjs/lib/options.js +7 -0
  39. package/dist-cjs/lib/options.js.map +2 -2
  40. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  41. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  42. package/dist-cjs/version.js +3 -3
  43. package/dist-cjs/version.js.map +1 -1
  44. package/dist-esm/index.d.mts +185 -9
  45. package/dist-esm/index.mjs +7 -1
  46. package/dist-esm/index.mjs.map +2 -2
  47. package/dist-esm/lib/TldrawEditor.mjs +3 -1
  48. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  49. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  50. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  51. package/dist-esm/lib/components/Shape.mjs +4 -26
  52. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  53. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  54. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  55. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  56. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  58. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  59. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  60. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
  62. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
  63. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  64. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  65. package/dist-esm/lib/editor/Editor.mjs +110 -59
  66. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  67. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
  68. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  69. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  70. package/dist-esm/lib/exports/getSvgJsx.mjs +2 -2
  71. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  72. package/dist-esm/lib/hooks/useCanvasEvents.mjs +25 -21
  73. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  74. package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
  75. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  76. package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
  77. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
  78. package/dist-esm/lib/license/Watermark.mjs +6 -6
  79. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  80. package/dist-esm/lib/options.mjs +7 -0
  81. package/dist-esm/lib/options.mjs.map +2 -2
  82. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  83. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  84. package/dist-esm/version.mjs +3 -3
  85. package/dist-esm/version.mjs.map +1 -1
  86. package/editor.css +297 -311
  87. package/package.json +12 -36
  88. package/src/index.ts +7 -0
  89. package/src/lib/TldrawEditor.tsx +7 -5
  90. package/src/lib/components/MenuClickCapture.tsx +0 -8
  91. package/src/lib/components/Shape.tsx +6 -21
  92. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  93. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  94. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  95. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  96. package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
  97. package/src/lib/config/TLUserPreferences.ts +8 -1
  98. package/src/lib/editor/Editor.test.ts +12 -11
  99. package/src/lib/editor/Editor.ts +141 -82
  100. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  101. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  102. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  103. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  104. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  105. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  106. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  107. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  108. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  109. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
  110. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
  111. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  112. package/src/lib/editor/types/misc-types.ts +73 -1
  113. package/src/lib/exports/getSvgJsx.tsx +2 -2
  114. package/src/lib/hooks/useCanvasEvents.ts +39 -32
  115. package/src/lib/hooks/useEditorComponents.tsx +7 -1
  116. package/src/lib/hooks/useStateAttribute.ts +15 -0
  117. package/src/lib/license/LicenseManager.test.ts +3 -1
  118. package/src/lib/license/Watermark.test.tsx +2 -1
  119. package/src/lib/license/Watermark.tsx +6 -6
  120. package/src/lib/options.ts +8 -0
  121. package/src/lib/utils/EditorAtom.ts +37 -0
  122. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  123. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  124. package/src/version.ts +3 -3
@@ -176,8 +176,10 @@ import {
176
176
  RequiredKeys,
177
177
  TLCameraMoveOptions,
178
178
  TLCameraOptions,
179
+ TLGetShapeAtPointOptions,
179
180
  TLImageExportOptions,
180
181
  TLSvgExportOptions,
182
+ TLUpdatePointerOptions,
181
183
  } from './types/misc-types'
182
184
  import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'
183
185
 
@@ -3072,7 +3074,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3072
3074
  // Dispatch a new pointer move because the pointer's page will have changed
3073
3075
  // (its screen position will compute to a new page position given the new camera position)
3074
3076
  const { currentScreenPoint, currentPagePoint } = this.inputs
3075
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
3076
3077
 
3077
3078
  // compare the next page point (derived from the current camera) to the current page point
3078
3079
  if (
@@ -3080,27 +3081,10 @@ export class Editor extends EventEmitter<TLEventMap> {
3080
3081
  currentScreenPoint.y / z - y !== currentPagePoint.y
3081
3082
  ) {
3082
3083
  // If it's changed, dispatch a pointer event
3083
- const event: TLPointerEventInfo = {
3084
- type: 'pointer',
3085
- target: 'canvas',
3086
- name: 'pointer_move',
3087
- // weird but true: we need to put the screen point back into client space
3088
- point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
3084
+ this.updatePointer({
3085
+ immediate: opts?.immediate,
3089
3086
  pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
3090
- ctrlKey: this.inputs.ctrlKey,
3091
- altKey: this.inputs.altKey,
3092
- shiftKey: this.inputs.shiftKey,
3093
- metaKey: this.inputs.metaKey,
3094
- accelKey: isAccelKey(this.inputs),
3095
- button: 0,
3096
- isPen: this.getInstanceState().isPenMode ?? false,
3097
- }
3098
-
3099
- if (opts?.immediate) {
3100
- this._flushEventForTick(event)
3101
- } else {
3102
- this.dispatch(event)
3103
- }
3087
+ })
3104
3088
  }
3105
3089
 
3106
3090
  this._tickCameraState()
@@ -4421,21 +4405,28 @@ export class Editor extends EventEmitter<TLEventMap> {
4421
4405
  */
4422
4406
  deletePage(page: TLPageId | TLPage): this {
4423
4407
  const id = typeof page === 'string' ? page : page.id
4424
- this.run(() => {
4425
- if (this.getIsReadonly()) return
4426
- const pages = this.getPages()
4427
- if (pages.length === 1) return
4408
+ this.run(
4409
+ () => {
4410
+ if (this.getIsReadonly()) return
4411
+ const pages = this.getPages()
4412
+ if (pages.length === 1) return
4428
4413
 
4429
- const deletedPage = this.getPage(id)
4430
- if (!deletedPage) return
4414
+ const deletedPage = this.getPage(id)
4415
+ if (!deletedPage) return
4431
4416
 
4432
- if (id === this.getCurrentPageId()) {
4433
- const index = pages.findIndex((page) => page.id === id)
4434
- const next = pages[index - 1] ?? pages[index + 1]
4435
- this.setCurrentPage(next.id)
4436
- }
4437
- this.store.remove([deletedPage.id])
4438
- })
4417
+ if (id === this.getCurrentPageId()) {
4418
+ const index = pages.findIndex((page) => page.id === id)
4419
+ const next = pages[index - 1] ?? pages[index + 1]
4420
+ this.setCurrentPage(next.id)
4421
+ }
4422
+
4423
+ const shapes = this.getSortedChildIdsForParent(deletedPage.id)
4424
+ this.deleteShapes(shapes)
4425
+
4426
+ this.store.remove([deletedPage.id])
4427
+ },
4428
+ { ignoreShapeLock: true }
4429
+ )
4439
4430
  return this
4440
4431
  }
4441
4432
 
@@ -5164,20 +5155,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5164
5155
  *
5165
5156
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5166
5157
  */
5167
- getShapeAtPoint(
5168
- point: VecLike,
5169
- opts = {} as {
5170
- renderingOnly?: boolean
5171
- margin?: number
5172
- hitInside?: boolean
5173
- hitLocked?: boolean
5174
- // TODO: we probably need to rename this, we don't quite _always_
5175
- // respect this esp. in the part below that does "Check labels first"
5176
- hitLabels?: boolean
5177
- hitFrameInside?: boolean
5178
- filter?(shape: TLShape): boolean
5179
- }
5180
- ): TLShape | undefined {
5158
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5181
5159
  const zoomLevel = this.getZoomLevel()
5182
5160
  const viewportPageBounds = this.getViewportPageBounds()
5183
5161
  const {
@@ -5189,6 +5167,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5189
5167
  hitFrameInside = false,
5190
5168
  } = opts
5191
5169
 
5170
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5171
+
5192
5172
  let inHollowSmallestArea = Infinity
5193
5173
  let inHollowSmallestAreaHit: TLShape | null = null
5194
5174
 
@@ -5208,7 +5188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5208
5188
  return false
5209
5189
  const pageMask = this.getShapeMask(shape)
5210
5190
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5211
- if (filter) return filter(shape)
5191
+ if (filter && !filter(shape)) return false
5212
5192
  return true
5213
5193
  })
5214
5194
 
@@ -5222,8 +5202,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5222
5202
  // Check labels first
5223
5203
  if (
5224
5204
  this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
5225
- (this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
5226
5205
  ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
5206
+ this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
5227
5207
  (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
5228
5208
  this.getShapeUtil(shape).getText(shape)?.trim())
5229
5209
  ) {
@@ -5234,13 +5214,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5234
5214
  }
5235
5215
  }
5236
5216
 
5237
- if (this.isShapeOfType(shape, 'frame')) {
5217
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5238
5218
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5239
5219
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5240
5220
 
5241
5221
  // If the hit is within the frame's outer margin, then select the frame
5242
- const distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5243
- if (Math.abs(distance) <= margin) {
5222
+ const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
5223
+ if (
5224
+ hitFrameInside
5225
+ ? (distance > 0 && distance <= outerMargin) ||
5226
+ (distance <= 0 && distance > -innerMargin)
5227
+ : distance > 0 && distance <= outerMargin
5228
+ ) {
5244
5229
  return inMarginClosestToEdgeHit || shape
5245
5230
  }
5246
5231
 
@@ -5279,11 +5264,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5279
5264
  // If the margin is zero and the geometry has a very small width or height,
5280
5265
  // then check the actual distance. This is to prevent a bug where straight
5281
5266
  // lines would never pass the broad phase (point-in-bounds) check.
5282
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5267
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5283
5268
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5284
5269
  } else {
5285
5270
  // Broad phase
5286
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5271
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5287
5272
  // Narrow phase (actual distance)
5288
5273
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5289
5274
  } else {
@@ -5298,7 +5283,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5283
  // the shape or negative if inside of the shape. If the distance
5299
5284
  // is greater than the margin, then it's a miss. Otherwise...
5300
5285
 
5301
- if (distance <= margin) {
5286
+ // Are we close to the shape's edge?
5287
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5302
5288
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5303
5289
  // If the shape is filled, then it's a hit. Remember, we're
5304
5290
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5308,11 +5294,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5308
5294
  // If the shape is bigger than the viewport, then skip it.
5309
5295
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5310
5296
 
5311
- // For hollow shapes...
5312
- if (Math.abs(distance) < margin) {
5313
- // We want to preference shapes where we're inside of the
5314
- // shape margin; and we would want to hit the shape with the
5315
- // edge closest to the point.
5297
+ // If we're close to the edge of the shape, and if it's the closest edge among
5298
+ // all the edges that we've gotten close to so far, then we will want to hit the
5299
+ // shape unless we hit something else or closer in later iterations.
5300
+ if (
5301
+ hitInside
5302
+ ? // On hitInside, the distance will be negative for hits inside
5303
+ // If the distance is positive, check against the outer margin
5304
+ (distance > 0 && distance <= outerMargin) ||
5305
+ // If the distance is negative, check against the inner margin
5306
+ (distance <= 0 && distance > -innerMargin)
5307
+ : // If hitInside is false, then sadly _we do not know_ whether the
5308
+ // point is inside or outside of the shape, so we check against
5309
+ // the max of the two margins
5310
+ Math.abs(distance) <= Math.max(innerMargin, outerMargin)
5311
+ ) {
5316
5312
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5317
5313
  inMarginClosestToEdgeDistance = Math.abs(distance)
5318
5314
  inMarginClosestToEdgeHit = shape
@@ -5334,6 +5330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5334
5330
  } else {
5335
5331
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5336
5332
  // If the distance is less than the margin, return the shape as the hit.
5333
+ // Use the editor's configurable hit test margin.
5337
5334
  if (distance < this.options.hitTestMargin / zoomLevel) {
5338
5335
  return shape
5339
5336
  }
@@ -6336,7 +6333,17 @@ export class Editor extends EventEmitter<TLEventMap> {
6336
6333
 
6337
6334
  this.createShapes(shapesToCreate)
6338
6335
  this.createBindings(bindingsToCreate)
6339
- this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))))
6336
+
6337
+ this.setSelectedShapes(
6338
+ compact(
6339
+ ids.map((oldId) => {
6340
+ const newId = shapeIds.get(oldId)
6341
+ if (!newId) return null
6342
+ if (!this.getShape(newId)) return null
6343
+ return newId
6344
+ })
6345
+ )
6346
+ )
6340
6347
 
6341
6348
  if (offset !== undefined) {
6342
6349
  // If we've offset the duplicated shapes, check to see whether their new bounds is entirely
@@ -7390,7 +7397,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7390
7397
  if (
7391
7398
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7392
7399
  type: 'stretch',
7393
- shapes: shapesToStretchFirstPass,
7394
7400
  })
7395
7401
  ) {
7396
7402
  continue
@@ -7861,25 +7867,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7861
7867
  ) {
7862
7868
  let parentId: TLParentId = this.getFocusedGroupId()
7863
7869
 
7864
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7865
- const parent = currentPageShapesSorted[i]
7866
- const util = this.getShapeUtil(parent)
7867
- if (
7868
- util.canReceiveNewChildrenOfType(parent, partial.type) &&
7869
- !this.isShapeHidden(parent) &&
7870
- this.isPointInShape(
7871
- parent,
7872
- // If no parent is provided, then we can treat the
7873
- // shape's provided x/y as being in the page's space.
7874
- { x: partial.x ?? 0, y: partial.y ?? 0 },
7875
- {
7876
- margin: 0,
7877
- hitInside: true,
7878
- }
7879
- )
7880
- ) {
7881
- parentId = parent.id
7882
- break
7870
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7871
+
7872
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7873
+ // that position. If not, we'll assume the user isn't deliberately placing the
7874
+ // shape and the positioning will be handled later by another system.
7875
+ if (isPositioned) {
7876
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7877
+ const parent = currentPageShapesSorted[i]
7878
+ const util = this.getShapeUtil(parent)
7879
+ if (
7880
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7881
+ !this.isShapeHidden(parent) &&
7882
+ this.isPointInShape(
7883
+ parent,
7884
+ // If no parent is provided, then we can treat the
7885
+ // shape's provided x/y as being in the page's space.
7886
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7887
+ {
7888
+ margin: 0,
7889
+ hitInside: true,
7890
+ }
7891
+ )
7892
+ ) {
7893
+ parentId = parent.id
7894
+ break
7895
+ }
7883
7896
  }
7884
7897
  }
7885
7898
 
@@ -9673,6 +9686,52 @@ export class Editor extends EventEmitter<TLEventMap> {
9673
9686
  return this
9674
9687
  }
9675
9688
 
9689
+ /**
9690
+ * Dispatch a pointer move event in the current position of the pointer. This is useful when
9691
+ * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want
9692
+ * the current interaction to respond to that change.
9693
+ *
9694
+ * @example
9695
+ * ```ts
9696
+ * editor.updatePointer()
9697
+ * ```
9698
+ *
9699
+ * @param options - The options for updating the pointer.
9700
+ * @returns The editor instance.
9701
+ * @public
9702
+ */
9703
+ updatePointer(options?: TLUpdatePointerOptions): this {
9704
+ const event: TLPointerEventInfo = {
9705
+ type: 'pointer',
9706
+ target: 'canvas',
9707
+ name: 'pointer_move',
9708
+ point:
9709
+ options?.point ??
9710
+ // weird but true: what `inputs` calls screen-space is actually viewport space. so
9711
+ // we need to convert back into true screen space first. we should fix this...
9712
+ Vec.Add(
9713
+ this.inputs.currentScreenPoint,
9714
+ this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9715
+ ),
9716
+ pointerId: options?.pointerId ?? 0,
9717
+ button: options?.button ?? 0,
9718
+ isPen: options?.isPen ?? this.inputs.isPen,
9719
+ shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9720
+ altKey: options?.altKey ?? this.inputs.altKey,
9721
+ ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9722
+ metaKey: options?.metaKey ?? this.inputs.metaKey,
9723
+ accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9724
+ }
9725
+
9726
+ if (options?.immediate) {
9727
+ this._flushEventForTick(event)
9728
+ } else {
9729
+ this.dispatch(event)
9730
+ }
9731
+
9732
+ return this
9733
+ }
9734
+
9676
9735
  /**
9677
9736
  * Puts the editor into focused mode.
9678
9737
  *
@@ -1,12 +1,13 @@
1
+ import { Mocked, vi } from 'vitest'
1
2
  import { Editor } from '../../Editor'
2
3
  import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
3
4
  import { ClickManager } from './ClickManager'
4
5
 
5
6
  // Mock the Editor class
6
- jest.mock('../../Editor')
7
+ vi.mock('../../Editor')
7
8
 
8
9
  describe('ClickManager', () => {
9
- let editor: jest.Mocked<Editor>
10
+ let editor: Mocked<Editor>
10
11
  let clickManager: ClickManager
11
12
  let mockTimers: any
12
13
 
@@ -29,14 +30,14 @@ describe('ClickManager', () => {
29
30
  })
30
31
 
31
32
  beforeEach(() => {
32
- jest.useFakeTimers()
33
+ vi.useFakeTimers()
33
34
  mockTimers = {
34
- setTimeout: jest.fn((fn, delay) => setTimeout(fn, delay)),
35
+ setTimeout: vi.fn((fn, delay) => setTimeout(fn, delay)),
35
36
  }
36
37
 
37
38
  editor = {
38
39
  timers: mockTimers,
39
- dispatch: jest.fn(),
40
+ dispatch: vi.fn(),
40
41
  options: {
41
42
  doubleClickDurationMs: 300,
42
43
  multiClickDurationMs: 300,
@@ -46,7 +47,7 @@ describe('ClickManager', () => {
46
47
  inputs: {
47
48
  currentScreenPoint: { x: 0, y: 0 },
48
49
  },
49
- getInstanceState: jest.fn(() => ({
50
+ getInstanceState: vi.fn(() => ({
50
51
  isCoarsePointer: false,
51
52
  })),
52
53
  } as any
@@ -55,8 +56,8 @@ describe('ClickManager', () => {
55
56
  })
56
57
 
57
58
  afterEach(() => {
58
- jest.useRealTimers()
59
- jest.clearAllMocks()
59
+ vi.useRealTimers()
60
+ vi.clearAllMocks()
60
61
  })
61
62
 
62
63
  describe('constructor and initial state', () => {
@@ -100,7 +101,7 @@ describe('ClickManager', () => {
100
101
  clickManager.handlePointerEvent(pointerEvent)
101
102
  expect(clickManager.clickState).toBe('pendingDouble')
102
103
 
103
- jest.advanceTimersByTime(350)
104
+ vi.advanceTimersByTime(350)
104
105
 
105
106
  expect(clickManager.clickState).toBe('idle')
106
107
  })
@@ -141,7 +142,7 @@ describe('ClickManager', () => {
141
142
  clickManager.handlePointerEvent(firstDown)
142
143
  clickManager.handlePointerEvent(secondDown)
143
144
 
144
- jest.advanceTimersByTime(350)
145
+ vi.advanceTimersByTime(350)
145
146
 
146
147
  expect(editor.dispatch).toHaveBeenCalledWith(
147
148
  expect.objectContaining({
@@ -235,7 +236,7 @@ describe('ClickManager', () => {
235
236
  clickManager.handlePointerEvent(pointerDown) // second
236
237
  clickManager.handlePointerEvent(pointerDown) // third
237
238
 
238
- jest.advanceTimersByTime(350)
239
+ vi.advanceTimersByTime(350)
239
240
 
240
241
  expect(editor.dispatch).toHaveBeenCalledWith(
241
242
  expect.objectContaining({
@@ -255,7 +256,7 @@ describe('ClickManager', () => {
255
256
  clickManager.handlePointerEvent(pointerDown) // third
256
257
  clickManager.handlePointerEvent(pointerDown) // fourth
257
258
 
258
- jest.advanceTimersByTime(350)
259
+ vi.advanceTimersByTime(350)
259
260
 
260
261
  expect(editor.dispatch).toHaveBeenCalledWith(
261
262
  expect.objectContaining({
@@ -277,7 +278,7 @@ describe('ClickManager', () => {
277
278
  editor.options.doubleClickDurationMs
278
279
  )
279
280
 
280
- jest.clearAllMocks()
281
+ vi.clearAllMocks()
281
282
 
282
283
  // Second click - should use multiClickDurationMs
283
284
  clickManager.handlePointerEvent(pointerDown)
@@ -392,7 +393,7 @@ describe('ClickManager', () => {
392
393
  clickManager.cancelDoubleClickTimeout()
393
394
 
394
395
  // Advance time - should not dispatch settle event
395
- jest.advanceTimersByTime(350)
396
+ vi.advanceTimersByTime(350)
396
397
 
397
398
  expect(editor.dispatch).not.toHaveBeenCalled()
398
399
  expect(clickManager.clickState).toBe('idle')
@@ -1,19 +1,20 @@
1
+ import { Mock, Mocked, vi } from 'vitest'
1
2
  import { Box } from '../../../primitives/Box'
2
3
  import { Vec } from '../../../primitives/Vec'
3
4
  import { Editor } from '../../Editor'
4
5
  import { EdgeScrollManager } from './EdgeScrollManager'
5
6
 
6
7
  // Mock the Editor class
7
- jest.mock('../../Editor')
8
+ vi.mock('../../Editor')
8
9
 
9
10
  describe('EdgeScrollManager', () => {
10
- let editor: jest.Mocked<
11
+ let editor: Mocked<
11
12
  Editor & {
12
- user: { getEdgeScrollSpeed: jest.Mock }
13
- getCamera: jest.Mock
14
- getCameraOptions: jest.Mock
15
- getZoomLevel: jest.Mock
16
- getViewportScreenBounds: jest.Mock
13
+ user: { getEdgeScrollSpeed: Mock }
14
+ getCamera: Mock
15
+ getCameraOptions: Mock
16
+ getZoomLevel: Mock
17
+ getViewportScreenBounds: Mock
17
18
  }
18
19
  >
19
20
  let edgeScrollManager: EdgeScrollManager
@@ -33,33 +34,33 @@ describe('EdgeScrollManager', () => {
33
34
  isPanning: false,
34
35
  },
35
36
  user: {
36
- getEdgeScrollSpeed: jest.fn(() => 1),
37
+ getEdgeScrollSpeed: vi.fn(() => 1),
37
38
  },
38
- getViewportScreenBounds: jest.fn(() => new Box(0, 0, 1000, 600)),
39
- getInstanceState: jest.fn(
39
+ getViewportScreenBounds: vi.fn(() => new Box(0, 0, 1000, 600)),
40
+ getInstanceState: vi.fn(
40
41
  () =>
41
42
  ({
42
43
  isCoarsePointer: false,
43
44
  insets: [false, false, false, false], // [top, right, bottom, left]
44
45
  }) as any
45
46
  ),
46
- getCameraOptions: jest.fn(() => ({
47
+ getCameraOptions: vi.fn(() => ({
47
48
  isLocked: false,
48
49
  panSpeed: 1,
49
50
  zoomSpeed: 1,
50
51
  zoomSteps: [1],
51
52
  wheelBehavior: 'pan' as const,
52
53
  })),
53
- getZoomLevel: jest.fn(() => 1),
54
- getCamera: jest.fn(() => new Vec(0, 0, 1)),
55
- setCamera: jest.fn(),
54
+ getZoomLevel: vi.fn(() => 1),
55
+ getCamera: vi.fn(() => new Vec(0, 0, 1)),
56
+ setCamera: vi.fn(),
56
57
  } as any
57
58
 
58
59
  edgeScrollManager = new EdgeScrollManager(editor as any)
59
60
  })
60
61
 
61
62
  afterEach(() => {
62
- jest.clearAllMocks()
63
+ vi.clearAllMocks()
63
64
  })
64
65
 
65
66
  describe('constructor and initialization', () => {