@tldraw/editor 3.15.4 → 3.16.0-canary.016d4c2889b7

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 (147) hide show
  1. package/dist-cjs/index.d.ts +180 -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 +8 -2
  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 +11 -36
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  14. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  19. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  20. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
  21. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
  22. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  23. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  24. package/dist-cjs/lib/editor/Editor.js +138 -69
  25. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
  27. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
  29. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  30. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  31. package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
  32. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  33. package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
  34. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  35. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  36. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  38. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  39. package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
  40. package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
  41. package/dist-cjs/lib/license/Watermark.js +6 -6
  42. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  43. package/dist-cjs/lib/options.js +7 -0
  44. package/dist-cjs/lib/options.js.map +2 -2
  45. package/dist-cjs/lib/primitives/Box.js +3 -0
  46. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  47. package/dist-cjs/lib/utils/EditorAtom.js +45 -0
  48. package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
  49. package/dist-cjs/version.js +3 -3
  50. package/dist-cjs/version.js.map +1 -1
  51. package/dist-esm/index.d.mts +180 -9
  52. package/dist-esm/index.mjs +7 -1
  53. package/dist-esm/index.mjs.map +2 -2
  54. package/dist-esm/lib/TldrawEditor.mjs +8 -2
  55. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  56. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  57. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  58. package/dist-esm/lib/components/Shape.mjs +11 -36
  59. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  63. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  64. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  67. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  69. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  70. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
  71. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
  72. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  73. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  74. package/dist-esm/lib/editor/Editor.mjs +138 -69
  75. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
  77. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
  79. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  80. package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
  81. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
  83. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
  85. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  87. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
  89. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
  90. package/dist-esm/lib/license/Watermark.mjs +6 -6
  91. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  92. package/dist-esm/lib/options.mjs +7 -0
  93. package/dist-esm/lib/options.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/Box.mjs +4 -1
  95. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  96. package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
  97. package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
  98. package/dist-esm/version.mjs +3 -3
  99. package/dist-esm/version.mjs.map +1 -1
  100. package/editor.css +305 -311
  101. package/package.json +14 -37
  102. package/src/index.ts +7 -0
  103. package/src/lib/TldrawEditor.tsx +13 -6
  104. package/src/lib/components/MenuClickCapture.tsx +0 -8
  105. package/src/lib/components/Shape.tsx +12 -33
  106. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
  107. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  108. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  109. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  110. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  111. package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
  112. package/src/lib/config/TLUserPreferences.ts +8 -1
  113. package/src/lib/editor/Editor.test.ts +12 -11
  114. package/src/lib/editor/Editor.ts +178 -103
  115. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  116. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  117. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  118. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  119. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  120. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  121. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  122. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  123. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  124. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
  125. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
  126. package/src/lib/editor/shapes/ShapeUtil.ts +36 -0
  127. package/src/lib/editor/types/misc-types.ts +73 -1
  128. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  129. package/src/lib/exports/getSvgJsx.tsx +78 -21
  130. package/src/lib/hooks/useCanvasEvents.ts +45 -38
  131. package/src/lib/hooks/useEditorComponents.tsx +7 -1
  132. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  133. package/src/lib/hooks/useStateAttribute.ts +15 -0
  134. package/src/lib/license/LicenseManager.test.ts +3 -1
  135. package/src/lib/license/Watermark.test.tsx +2 -1
  136. package/src/lib/license/Watermark.tsx +6 -6
  137. package/src/lib/options.ts +8 -0
  138. package/src/lib/primitives/Box.test.ts +126 -0
  139. package/src/lib/primitives/Box.ts +10 -1
  140. package/src/lib/utils/EditorAtom.ts +37 -0
  141. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  142. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  143. package/src/version.ts +3 -3
  144. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  145. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  146. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  147. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -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
 
@@ -4869,27 +4860,25 @@ export class Editor extends EventEmitter<TLEventMap> {
4869
4860
  return this.store.createComputedCache('pageMaskCache', (shape) => {
4870
4861
  if (isPageId(shape.parentId)) return undefined
4871
4862
 
4872
- const frameAncestors = this.getShapeAncestors(shape.id).filter((shape) =>
4873
- this.isShapeOfType<TLFrameShape>(shape, 'frame')
4874
- )
4875
-
4876
- if (frameAncestors.length === 0) return undefined
4877
-
4878
- const pageMask = frameAncestors
4879
- .map<Vec[] | undefined>((s) => {
4880
- // Apply the frame transform to the frame outline to get the frame outline in the current page space
4881
- const geometry = this.getShapeGeometry(s.id)
4882
- const pageTransform = this.getShapePageTransform(s.id)
4883
- return pageTransform.applyToPoints(geometry.vertices)
4884
- })
4885
- .reduce((acc, b) => {
4886
- if (!(b && acc)) return undefined
4887
- const intersection = intersectPolygonPolygon(acc, b)
4888
- if (intersection) {
4889
- return intersection.map(Vec.Cast)
4890
- }
4891
- return []
4892
- })
4863
+ const clipPaths: Vec[][] = []
4864
+ // Get all ancestors that can potentially clip this shape
4865
+ for (const ancestor of this.getShapeAncestors(shape.id)) {
4866
+ const util = this.getShapeUtil(ancestor)
4867
+ const clipPath = util.getClipPath?.(ancestor)
4868
+ if (!clipPath) continue
4869
+ if (util.shouldClipChild?.(shape) === false) continue
4870
+ const pageTransform = this.getShapePageTransform(ancestor.id)
4871
+ clipPaths.push(pageTransform.applyToPoints(clipPath))
4872
+ }
4873
+ if (clipPaths.length === 0) return undefined
4874
+
4875
+ const pageMask = clipPaths.reduce((acc, b) => {
4876
+ const intersection = intersectPolygonPolygon(acc, b)
4877
+ if (intersection) {
4878
+ return intersection.map(Vec.Cast)
4879
+ }
4880
+ return []
4881
+ })
4893
4882
 
4894
4883
  return pageMask
4895
4884
  })
@@ -5164,20 +5153,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5164
5153
  *
5165
5154
  * @returns The shape at the given point, or undefined if there is no shape at the point.
5166
5155
  */
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 {
5156
+ getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
5181
5157
  const zoomLevel = this.getZoomLevel()
5182
5158
  const viewportPageBounds = this.getViewportPageBounds()
5183
5159
  const {
@@ -5189,6 +5165,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5189
5165
  hitFrameInside = false,
5190
5166
  } = opts
5191
5167
 
5168
+ const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
5169
+
5192
5170
  let inHollowSmallestArea = Infinity
5193
5171
  let inHollowSmallestAreaHit: TLShape | null = null
5194
5172
 
@@ -5208,7 +5186,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5208
5186
  return false
5209
5187
  const pageMask = this.getShapeMask(shape)
5210
5188
  if (pageMask && !pointInPolygon(point, pageMask)) return false
5211
- if (filter) return filter(shape)
5189
+ if (filter && !filter(shape)) return false
5212
5190
  return true
5213
5191
  })
5214
5192
 
@@ -5222,8 +5200,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5222
5200
  // Check labels first
5223
5201
  if (
5224
5202
  this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
5225
- (this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
5226
5203
  ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
5204
+ this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
5227
5205
  (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
5228
5206
  this.getShapeUtil(shape).getText(shape)?.trim())
5229
5207
  ) {
@@ -5234,13 +5212,18 @@ export class Editor extends EventEmitter<TLEventMap> {
5234
5212
  }
5235
5213
  }
5236
5214
 
5237
- if (this.isShapeOfType(shape, 'frame')) {
5215
+ if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
5238
5216
  // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
5239
5217
  // this prevents clicks from passing through the body of a frame to shapes behind it.
5240
5218
 
5241
5219
  // 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) {
5220
+ const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
5221
+ if (
5222
+ hitFrameInside
5223
+ ? (distance > 0 && distance <= outerMargin) ||
5224
+ (distance <= 0 && distance > -innerMargin)
5225
+ : distance > 0 && distance <= outerMargin
5226
+ ) {
5244
5227
  return inMarginClosestToEdgeHit || shape
5245
5228
  }
5246
5229
 
@@ -5279,11 +5262,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5279
5262
  // If the margin is zero and the geometry has a very small width or height,
5280
5263
  // then check the actual distance. This is to prevent a bug where straight
5281
5264
  // lines would never pass the broad phase (point-in-bounds) check.
5282
- if (margin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5265
+ if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
5283
5266
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5284
5267
  } else {
5285
5268
  // Broad phase
5286
- if (geometry.bounds.containsPoint(pointInShapeSpace, margin)) {
5269
+ if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
5287
5270
  // Narrow phase (actual distance)
5288
5271
  distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
5289
5272
  } else {
@@ -5298,7 +5281,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5298
5281
  // the shape or negative if inside of the shape. If the distance
5299
5282
  // is greater than the margin, then it's a miss. Otherwise...
5300
5283
 
5301
- if (distance <= margin) {
5284
+ // Are we close to the shape's edge?
5285
+ if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
5302
5286
  if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
5303
5287
  // If the shape is filled, then it's a hit. Remember, we're
5304
5288
  // starting from the TOP-MOST shape in z-index order, so any
@@ -5308,11 +5292,21 @@ export class Editor extends EventEmitter<TLEventMap> {
5308
5292
  // If the shape is bigger than the viewport, then skip it.
5309
5293
  if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
5310
5294
 
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.
5295
+ // If we're close to the edge of the shape, and if it's the closest edge among
5296
+ // all the edges that we've gotten close to so far, then we will want to hit the
5297
+ // shape unless we hit something else or closer in later iterations.
5298
+ if (
5299
+ hitInside
5300
+ ? // On hitInside, the distance will be negative for hits inside
5301
+ // If the distance is positive, check against the outer margin
5302
+ (distance > 0 && distance <= outerMargin) ||
5303
+ // If the distance is negative, check against the inner margin
5304
+ (distance <= 0 && distance > -innerMargin)
5305
+ : // If hitInside is false, then sadly _we do not know_ whether the
5306
+ // point is inside or outside of the shape, so we check against
5307
+ // the max of the two margins
5308
+ Math.abs(distance) <= Math.max(innerMargin, outerMargin)
5309
+ ) {
5316
5310
  if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
5317
5311
  inMarginClosestToEdgeDistance = Math.abs(distance)
5318
5312
  inMarginClosestToEdgeHit = shape
@@ -5334,6 +5328,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5334
5328
  } else {
5335
5329
  // For open shapes (e.g. lines or draw shapes) always use the margin.
5336
5330
  // If the distance is less than the margin, return the shape as the hit.
5331
+ // Use the editor's configurable hit test margin.
5337
5332
  if (distance < this.options.hitTestMargin / zoomLevel) {
5338
5333
  return shape
5339
5334
  }
@@ -6336,7 +6331,17 @@ export class Editor extends EventEmitter<TLEventMap> {
6336
6331
 
6337
6332
  this.createShapes(shapesToCreate)
6338
6333
  this.createBindings(bindingsToCreate)
6339
- this.setSelectedShapes(compact(ids.map((id) => shapeIds.get(id))))
6334
+
6335
+ this.setSelectedShapes(
6336
+ compact(
6337
+ ids.map((oldId) => {
6338
+ const newId = shapeIds.get(oldId)
6339
+ if (!newId) return null
6340
+ if (!this.getShape(newId)) return null
6341
+ return newId
6342
+ })
6343
+ )
6344
+ )
6340
6345
 
6341
6346
  if (offset !== undefined) {
6342
6347
  // If we've offset the duplicated shapes, check to see whether their new bounds is entirely
@@ -7390,7 +7395,6 @@ export class Editor extends EventEmitter<TLEventMap> {
7390
7395
  if (
7391
7396
  !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7392
7397
  type: 'stretch',
7393
- shapes: shapesToStretchFirstPass,
7394
7398
  })
7395
7399
  ) {
7396
7400
  continue
@@ -7861,25 +7865,32 @@ export class Editor extends EventEmitter<TLEventMap> {
7861
7865
  ) {
7862
7866
  let parentId: TLParentId = this.getFocusedGroupId()
7863
7867
 
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
7868
+ const isPositioned = partial.x !== undefined && partial.y !== undefined
7869
+
7870
+ // If the shape has been explicitly positioned, we'll try to find a parent at
7871
+ // that position. If not, we'll assume the user isn't deliberately placing the
7872
+ // shape and the positioning will be handled later by another system.
7873
+ if (isPositioned) {
7874
+ for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7875
+ const parent = currentPageShapesSorted[i]
7876
+ const util = this.getShapeUtil(parent)
7877
+ if (
7878
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7879
+ !this.isShapeHidden(parent) &&
7880
+ this.isPointInShape(
7881
+ parent,
7882
+ // If no parent is provided, then we can treat the
7883
+ // shape's provided x/y as being in the page's space.
7884
+ { x: partial.x ?? 0, y: partial.y ?? 0 },
7885
+ {
7886
+ margin: 0,
7887
+ hitInside: true,
7888
+ }
7889
+ )
7890
+ ) {
7891
+ parentId = parent.id
7892
+ break
7893
+ }
7883
7894
  }
7884
7895
  }
7885
7896
 
@@ -9506,6 +9517,24 @@ export class Editor extends EventEmitter<TLEventMap> {
9506
9517
  }
9507
9518
  }
9508
9519
 
9520
+ /**
9521
+ * Get an exported image of the given shapes as a data URL.
9522
+ *
9523
+ * @param shapes - The shapes (or shape ids) to export.
9524
+ * @param opts - Options for the export.
9525
+ *
9526
+ * @returns A data URL of the image.
9527
+ * @public
9528
+ */
9529
+ async toImageDataUrl(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
9530
+ const { blob, width, height } = await this.toImage(shapes, opts)
9531
+ return {
9532
+ url: await FileHelpers.blobToDataUrl(blob),
9533
+ width,
9534
+ height,
9535
+ }
9536
+ }
9537
+
9509
9538
  /* --------------------- Events --------------------- */
9510
9539
 
9511
9540
  /**
@@ -9673,6 +9702,52 @@ export class Editor extends EventEmitter<TLEventMap> {
9673
9702
  return this
9674
9703
  }
9675
9704
 
9705
+ /**
9706
+ * Dispatch a pointer move event in the current position of the pointer. This is useful when
9707
+ * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want
9708
+ * the current interaction to respond to that change.
9709
+ *
9710
+ * @example
9711
+ * ```ts
9712
+ * editor.updatePointer()
9713
+ * ```
9714
+ *
9715
+ * @param options - The options for updating the pointer.
9716
+ * @returns The editor instance.
9717
+ * @public
9718
+ */
9719
+ updatePointer(options?: TLUpdatePointerOptions): this {
9720
+ const event: TLPointerEventInfo = {
9721
+ type: 'pointer',
9722
+ target: 'canvas',
9723
+ name: 'pointer_move',
9724
+ point:
9725
+ options?.point ??
9726
+ // weird but true: what `inputs` calls screen-space is actually viewport space. so
9727
+ // we need to convert back into true screen space first. we should fix this...
9728
+ Vec.Add(
9729
+ this.inputs.currentScreenPoint,
9730
+ this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9731
+ ),
9732
+ pointerId: options?.pointerId ?? 0,
9733
+ button: options?.button ?? 0,
9734
+ isPen: options?.isPen ?? this.inputs.isPen,
9735
+ shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9736
+ altKey: options?.altKey ?? this.inputs.altKey,
9737
+ ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9738
+ metaKey: options?.metaKey ?? this.inputs.metaKey,
9739
+ accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9740
+ }
9741
+
9742
+ if (options?.immediate) {
9743
+ this._flushEventForTick(event)
9744
+ } else {
9745
+ this.dispatch(event)
9746
+ }
9747
+
9748
+ return this
9749
+ }
9750
+
9676
9751
  /**
9677
9752
  * Puts the editor into focused mode.
9678
9753
  *
@@ -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', () => {