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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +446 -120
  3. package/dist-cjs/index.js +8 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  7. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -5
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/constants.js +1 -3
  11. package/dist-cjs/lib/constants.js.map +2 -2
  12. package/dist-cjs/lib/editor/Editor.js +342 -280
  13. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  14. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -23
  15. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  17. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  19. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  21. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  23. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  25. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  27. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  28. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +181 -0
  29. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  30. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  31. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  33. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  34. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  35. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  36. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  37. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  38. package/dist-cjs/lib/exports/parseCss.js +1 -1
  39. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  40. package/dist-cjs/lib/globals/environment.js +45 -9
  41. package/dist-cjs/lib/globals/environment.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  43. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  44. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  45. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  48. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  49. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  52. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  53. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  54. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  55. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  56. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  57. package/dist-cjs/lib/options.js +6 -1
  58. package/dist-cjs/lib/options.js.map +2 -2
  59. package/dist-cjs/lib/primitives/Box.js +3 -0
  60. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  62. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  63. package/dist-cjs/lib/utils/rotation.js +1 -1
  64. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  65. package/dist-cjs/version.js +3 -3
  66. package/dist-cjs/version.js.map +1 -1
  67. package/dist-esm/index.d.mts +446 -120
  68. package/dist-esm/index.mjs +9 -2
  69. package/dist-esm/index.mjs.map +2 -2
  70. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  71. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  72. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  73. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -5
  74. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  75. package/dist-esm/lib/constants.mjs +1 -3
  76. package/dist-esm/lib/constants.mjs.map +2 -2
  77. package/dist-esm/lib/editor/Editor.mjs +343 -283
  78. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  79. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -23
  80. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  81. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  82. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  83. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  84. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  85. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  86. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  87. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  88. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  89. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  90. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  92. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  93. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +161 -0
  94. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  95. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  96. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  97. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  98. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  99. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  100. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  101. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  102. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  103. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  104. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  105. package/dist-esm/lib/globals/environment.mjs +45 -9
  106. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  107. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  108. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  109. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  110. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  111. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  112. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  113. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  114. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  115. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  116. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  117. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  118. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  119. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  120. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  121. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  122. package/dist-esm/lib/options.mjs +6 -1
  123. package/dist-esm/lib/options.mjs.map +2 -2
  124. package/dist-esm/lib/primitives/Box.mjs +3 -0
  125. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  126. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  127. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  128. package/dist-esm/lib/utils/rotation.mjs +1 -1
  129. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  130. package/dist-esm/version.mjs +3 -3
  131. package/dist-esm/version.mjs.map +1 -1
  132. package/editor.css +14 -12
  133. package/package.json +21 -17
  134. package/src/index.ts +5 -1
  135. package/src/lib/components/ErrorBoundary.tsx +1 -1
  136. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  137. package/src/lib/components/default-components/DefaultCanvas.tsx +4 -8
  138. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  139. package/src/lib/constants.ts +0 -2
  140. package/src/lib/editor/Editor.test.ts +140 -0
  141. package/src/lib/editor/Editor.ts +448 -326
  142. package/src/lib/editor/derivations/notVisibleShapes.ts +21 -33
  143. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  144. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  145. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  146. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  147. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  148. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  149. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  150. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  151. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  152. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  153. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +215 -0
  154. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  155. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  156. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  157. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  158. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  159. package/src/lib/exports/parseCss.test.ts +1 -0
  160. package/src/lib/exports/parseCss.ts +1 -1
  161. package/src/lib/globals/environment.ts +65 -10
  162. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  163. package/src/lib/hooks/useEvent.tsx +1 -1
  164. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  165. package/src/lib/hooks/useGestureEvents.ts +2 -2
  166. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  167. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  168. package/src/lib/hooks/useScreenBounds.ts +1 -1
  169. package/src/lib/hooks/useStateAttribute.ts +4 -1
  170. package/src/lib/hooks/useTransform.ts +1 -1
  171. package/src/lib/hooks/useZoomCss.ts +3 -8
  172. package/src/lib/options.ts +32 -0
  173. package/src/lib/primitives/Box.ts +9 -0
  174. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  175. package/src/lib/utils/rotation.ts +1 -1
  176. package/src/version.ts +3 -3
@@ -190,7 +190,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
190
190
  * @param shape - The shape.
191
191
  * @public
192
192
  */
193
- canSnap(_shape: Shape): boolean {
193
+ canSnap(shape: Shape): boolean {
194
194
  return true
195
195
  }
196
196
 
@@ -200,7 +200,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
200
200
  * @param shape - The shape.
201
201
  * @public
202
202
  */
203
- canTabTo(_shape: Shape): boolean {
203
+ canTabTo(shape: Shape): boolean {
204
204
  return true
205
205
  }
206
206
 
@@ -209,7 +209,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
209
209
  *
210
210
  * @public
211
211
  */
212
- canScroll(_shape: Shape): boolean {
212
+ canScroll(shape: Shape): boolean {
213
213
  return false
214
214
  }
215
215
 
@@ -227,7 +227,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
227
227
  *
228
228
  * @public
229
229
  */
230
- canEdit(_shape: Shape): boolean {
230
+ canEdit(shape: Shape, info: TLEditStartInfo): boolean {
231
231
  return false
232
232
  }
233
233
 
@@ -236,7 +236,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
236
236
  *
237
237
  * @public
238
238
  */
239
- canResize(_shape: Shape): boolean {
239
+ canResize(shape: Shape): boolean {
240
240
  return true
241
241
  }
242
242
 
@@ -245,7 +245,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
245
245
  *
246
246
  * @public
247
247
  */
248
- canResizeChildren(_shape: Shape): boolean {
248
+ canResizeChildren(shape: Shape): boolean {
249
249
  return true
250
250
  }
251
251
 
@@ -254,7 +254,16 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
254
254
  *
255
255
  * @public
256
256
  */
257
- canEditInReadonly(_shape: Shape): boolean {
257
+ canEditInReadonly(shape: Shape): boolean {
258
+ return false
259
+ }
260
+
261
+ /**
262
+ * Whether the shape can be edited while locked or while an ancestor is locked.
263
+ *
264
+ * @public
265
+ */
266
+ canEditWhileLocked(shape: Shape): boolean {
258
267
  return false
259
268
  }
260
269
 
@@ -263,7 +272,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
263
272
  *
264
273
  * @public
265
274
  */
266
- canCrop(_shape: Shape): boolean {
275
+ canCrop(shape: Shape): boolean {
267
276
  return false
268
277
  }
269
278
 
@@ -276,7 +285,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
276
285
  *
277
286
  * @public
278
287
  */
279
- canBeLaidOut(_shape: Shape, _info: TLShapeUtilCanBeLaidOutOpts): boolean {
288
+ canBeLaidOut(shape: Shape, info: TLShapeUtilCanBeLaidOutOpts): boolean {
280
289
  return true
281
290
  }
282
291
 
@@ -287,7 +296,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
287
296
  *
288
297
  * @param shape - The shape.
289
298
  */
290
- canCull(_shape: Shape): boolean {
299
+ canCull(shape: Shape): boolean {
291
300
  return true
292
301
  }
293
302
 
@@ -300,13 +309,33 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
300
309
  *
301
310
  * @internal
302
311
  */
303
- providesBackgroundForChildren(_shape: Shape): boolean {
312
+ providesBackgroundForChildren(shape: Shape): boolean {
304
313
  return false
305
314
  }
306
315
 
307
316
  /**
308
317
  * Get the clip path to apply to this shape's children.
309
318
  *
319
+ * The returned points should define the **inner** clip boundary - the area where
320
+ * children will be visible. If your shape has a stroke, you should inset the clip
321
+ * path by half the stroke width so children are clipped to the inner edge of the
322
+ * stroke rather than its center line.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * override getClipPath(shape: MyShape): Vec[] | undefined {
327
+ * const strokeWidth = 2
328
+ * const inset = strokeWidth / 2
329
+ * // Return points inset by half the stroke width
330
+ * return [
331
+ * new Vec(inset, inset),
332
+ * new Vec(shape.props.w - inset, inset),
333
+ * new Vec(shape.props.w - inset, shape.props.h - inset),
334
+ * new Vec(inset, shape.props.h - inset),
335
+ * ]
336
+ * }
337
+ * ```
338
+ *
310
339
  * @param shape - The shape to get the clip path for
311
340
  * @returns Array of points defining the clipping polygon in local coordinates, or undefined if no clipping
312
341
  * @public
@@ -333,14 +362,14 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
333
362
  * @returns boolean indicating if this shape should hide in the minimap
334
363
  * @public
335
364
  */
336
- hideInMinimap?(_shape: Shape): boolean
365
+ hideInMinimap?(shape: Shape): boolean
337
366
 
338
367
  /**
339
368
  * Whether the shape should hide its resize handles when selected.
340
369
  *
341
370
  * @public
342
371
  */
343
- hideResizeHandles(_shape: Shape): boolean {
372
+ hideResizeHandles(shape: Shape): boolean {
344
373
  return false
345
374
  }
346
375
 
@@ -349,7 +378,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
349
378
  *
350
379
  * @public
351
380
  */
352
- hideRotateHandle(_shape: Shape): boolean {
381
+ hideRotateHandle(shape: Shape): boolean {
353
382
  return false
354
383
  }
355
384
 
@@ -358,7 +387,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
358
387
  *
359
388
  * @public
360
389
  */
361
- hideSelectionBoundsBg(_shape: Shape): boolean {
390
+ hideSelectionBoundsBg(shape: Shape): boolean {
362
391
  return false
363
392
  }
364
393
 
@@ -367,7 +396,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
367
396
  *
368
397
  * @public
369
398
  */
370
- hideSelectionBoundsFg(_shape: Shape): boolean {
399
+ hideSelectionBoundsFg(shape: Shape): boolean {
371
400
  return false
372
401
  }
373
402
 
@@ -376,7 +405,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
376
405
  *
377
406
  * @public
378
407
  */
379
- isAspectRatioLocked(_shape: Shape): boolean {
408
+ isAspectRatioLocked(shape: Shape): boolean {
380
409
  return false
381
410
  }
382
411
 
@@ -387,10 +416,10 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
387
416
  * useful in cases like annotating on top of an image, where you usually want to avoid extra
388
417
  * padding around the image if you don't need it.
389
418
  *
390
- * @param _shape - The shape to check
419
+ * @param shape - The shape to check
391
420
  * @returns True if this shape should be treated as an export bounds container
392
421
  */
393
- isExportBoundsContainer(_shape: Shape): boolean {
422
+ isExportBoundsContainer(shape: Shape): boolean {
394
423
  return false
395
424
  }
396
425
 
@@ -439,7 +468,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
439
468
  * @param type - The shape type.
440
469
  * @public
441
470
  */
442
- canReceiveNewChildrenOfType(_shape: Shape, _type: TLShape['type']) {
471
+ canReceiveNewChildrenOfType(shape: Shape, _type: TLShape['type']) {
443
472
  return false
444
473
  }
445
474
 
@@ -487,7 +516,7 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
487
516
  * Get the geometry to use when snapping to this this shape in translate/resize operations. See
488
517
  * {@link BoundsSnapGeometry} for details.
489
518
  */
490
- getBoundsSnapGeometry(_shape: Shape): BoundsSnapGeometry {
519
+ getBoundsSnapGeometry(shape: Shape): BoundsSnapGeometry {
491
520
  return {}
492
521
  }
493
522
 
@@ -495,15 +524,15 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
495
524
  * Get the geometry to use when snapping handles to this shape. See {@link HandleSnapGeometry}
496
525
  * for details.
497
526
  */
498
- getHandleSnapGeometry(_shape: Shape): HandleSnapGeometry {
527
+ getHandleSnapGeometry(shape: Shape): HandleSnapGeometry {
499
528
  return {}
500
529
  }
501
530
 
502
- getText(_shape: Shape): string | undefined {
531
+ getText(shape: Shape): string | undefined {
503
532
  return undefined
504
533
  }
505
534
 
506
- getAriaDescriptor(_shape: Shape): string | undefined {
535
+ getAriaDescriptor(shape: Shape): string | undefined {
507
536
  return undefined
508
537
  }
509
538
 
@@ -931,3 +960,17 @@ export interface TLHandleDragInfo<T extends TLShape> {
931
960
  isCreatingShape: boolean
932
961
  initial?: T | undefined
933
962
  }
963
+
964
+ /* --------------------------------- Editing -------------------------------- */
965
+
966
+ /** @public */
967
+ export interface TLEditStartInfo {
968
+ type:
969
+ | 'press_enter'
970
+ | 'click'
971
+ | 'double-click'
972
+ | 'double-click-edge'
973
+ | 'double-click-corner'
974
+ | 'click-header'
975
+ | 'unknown'
976
+ }
@@ -6,7 +6,7 @@ import { getPerfectDashProps } from '../shared/getPerfectDashProps'
6
6
  export function DashedOutlineBox({ bounds, className }: { bounds: Box; className: string }) {
7
7
  const editor = useEditor()
8
8
 
9
- const zoomLevel = useValue('zoom level', () => editor.getZoomLevel(), [editor])
9
+ const zoomLevel = useValue('zoom level', () => editor.getEfficientZoomLevel(), [editor])
10
10
 
11
11
  return (
12
12
  <g className={className} pointerEvents="none" strokeLinecap="round" strokeLinejoin="round">
@@ -12,8 +12,8 @@ export class Pointing extends StateNode {
12
12
 
13
13
  override onPointerMove(info: TLPointerEventInfo) {
14
14
  const { editor } = this
15
- if (editor.inputs.isDragging) {
16
- const { originPagePoint } = editor.inputs
15
+ if (editor.inputs.getIsDragging()) {
16
+ const originPagePoint = editor.inputs.getOriginPagePoint()
17
17
 
18
18
  const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType
19
19
 
@@ -78,7 +78,7 @@ export class Pointing extends StateNode {
78
78
  }
79
79
 
80
80
  complete() {
81
- const { originPagePoint } = this.editor.inputs
81
+ const originPagePoint = this.editor.inputs.getOriginPagePoint()
82
82
 
83
83
  const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType as TLBaseBoxShape['type']
84
84
 
@@ -364,4 +364,5 @@ test('parseCssValueUrls', () => {
364
364
  },
365
365
  ]
366
366
  `)
367
+ expect(parseCssValueUrls(`url(#arrowhead)`)).toMatchInlineSnapshot(`[]`)
367
368
  })
@@ -108,5 +108,5 @@ export function parseCssValueUrls(value: string) {
108
108
  return Array.from(value.matchAll(urlsRegex), (m) => ({
109
109
  original: m[0],
110
110
  url: m[1] || m[2] || m[3],
111
- }))
111
+ })).filter((m) => !m.url.startsWith('#'))
112
112
  }
@@ -1,5 +1,9 @@
1
+ import { atom } from '@tldraw/state'
2
+
1
3
  /**
2
4
  * An object that contains information about the current device and environment.
5
+ * This object is not reactive and will not update automatically when the environment changes,
6
+ * so only include values that are fixed, such as the user's browser and operating system.
3
7
  *
4
8
  * @public
5
9
  */
@@ -14,15 +18,66 @@ const tlenv = {
14
18
  hasCanvasSupport: false,
15
19
  }
16
20
 
17
- if (typeof window !== 'undefined' && 'navigator' in window) {
18
- tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
19
- tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
20
- tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)
21
- tlenv.isFirefox = /firefox/i.test(navigator.userAgent)
22
- tlenv.isAndroid = /android/i.test(navigator.userAgent)
23
- tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
24
- tlenv.hasCanvasSupport =
25
- typeof window !== 'undefined' && 'Promise' in window && 'HTMLCanvasElement' in window
21
+ let isForcedFinePointer = false
22
+
23
+ if (typeof window !== 'undefined') {
24
+ if ('navigator' in window) {
25
+ tlenv.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
26
+ tlenv.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
27
+ tlenv.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)
28
+ tlenv.isFirefox = /firefox/i.test(navigator.userAgent)
29
+ tlenv.isAndroid = /android/i.test(navigator.userAgent)
30
+ tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
31
+ }
32
+ tlenv.hasCanvasSupport = 'Promise' in window && 'HTMLCanvasElement' in window
33
+ isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
34
+ }
35
+
36
+ /**
37
+ * An atom that contains information about the current device and environment.
38
+ * This object is reactive and will update automatically when the environment changes.
39
+ * Use it for values that may change over time, such as the pointer type.
40
+ *
41
+ * @public
42
+ */
43
+ const tlenvReactive = atom('tlenvReactive', {
44
+ // Whether the user's device has a coarse pointer. This is dynamic on many systems, especially
45
+ // on touch-screen laptops, which will become "coarse" if the user touches the screen.
46
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer#coarse
47
+ isCoarsePointer: false,
48
+ })
49
+
50
+ if (typeof window !== 'undefined' && !isForcedFinePointer) {
51
+ const mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')
52
+
53
+ const isCurrentCoarsePointer = () => tlenvReactive.__unsafe__getWithoutCapture().isCoarsePointer
54
+
55
+ if (mql) {
56
+ // 1. Update the coarse pointer automatically when the media query changes
57
+ const updateIsCoarsePointer = () => {
58
+ const isCoarsePointer = mql.matches
59
+ if (isCoarsePointer !== isCurrentCoarsePointer()) {
60
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarsePointer }))
61
+ }
62
+ }
63
+ updateIsCoarsePointer()
64
+ mql.addEventListener('change', updateIsCoarsePointer)
65
+ }
66
+
67
+ // 2. Also update the coarse pointer state when a pointer down event occurs. We need `capture: true`
68
+ // here because the tldraw component itself stops propagation on pointer events it receives.
69
+ window.addEventListener(
70
+ 'pointerdown',
71
+ (e: PointerEvent) => {
72
+ // when the user interacts with a mouse, we assume they have a fine pointer.
73
+ // otherwise, we assume they have a coarse pointer.
74
+ const isCoarseEvent = e.pointerType !== 'mouse'
75
+ if (isCoarseEvent !== isCurrentCoarsePointer()) {
76
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: isCoarseEvent }))
77
+ }
78
+ },
79
+ { capture: true }
80
+ )
26
81
  }
27
82
 
28
- export { tlenv }
83
+ export { tlenv, tlenvReactive }
@@ -1,66 +1,23 @@
1
- import { useEffect } from 'react'
2
- import { tlenv } from '../globals/environment'
1
+ import { unsafe__withoutCapture } from '@tldraw/state'
2
+ import { useReactor } from '@tldraw/state-react'
3
+ import { tlenvReactive } from '../globals/environment'
3
4
  import { useEditor } from './useEditor'
4
5
 
5
6
  /** @internal */
6
7
  export function useCoarsePointer() {
7
8
  const editor = useEditor()
8
9
 
9
- useEffect(() => {
10
- // We'll track our own state for the pointer type
11
- let isCoarse = editor.getInstanceState().isCoarsePointer
12
-
13
- // 1.
14
- // We'll use pointer events to detect coarse pointer.
15
-
16
- const handlePointerDown = (e: PointerEvent) => {
17
- // when the user interacts with a mouse, we assume they have a fine pointer.
18
- // otherwise, we assume they have a coarse pointer.
19
- const isCoarseEvent = e.pointerType !== 'mouse'
20
- if (isCoarse === isCoarseEvent) return
21
- isCoarse = isCoarseEvent
22
- editor.updateInstanceState({ isCoarsePointer: isCoarseEvent })
23
- }
24
-
25
- // we need `capture: true` here because the tldraw component itself stops propagation on
26
- // pointer events it receives.
27
- window.addEventListener('pointerdown', handlePointerDown, { capture: true })
28
-
29
- // 2.
30
- // We can also use the media query to detect / set the initial pointer type
31
- // and update the state if the pointer type changes.
32
-
33
- // We want the touch / mouse events to run even if the browser does not
34
- // support matchMedia. We'll have to handle the media query changes
35
- // conditionally in the code below.
36
- const mql = window.matchMedia && window.matchMedia('(any-pointer: coarse)')
37
-
38
- // This is a workaround for a Firefox bug where we don't correctly
39
- // detect coarse VS fine pointer. For now, let's assume that you have a fine
40
- // pointer if you're on Firefox on desktop.
41
- const isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
42
-
43
- const handleMediaQueryChange = () => {
44
- const next = isForcedFinePointer ? false : mql.matches // get the value from the media query
45
- if (isCoarse !== next) return // bail if the value hasn't changed
46
- isCoarse = next // update the local value
47
- editor.updateInstanceState({ isCoarsePointer: next }) // update the value in state
48
- }
49
-
50
- if (mql) {
51
- // set up the listener
52
- mql.addEventListener('change', handleMediaQueryChange)
53
-
54
- // and run the handler once to set the initial value
55
- handleMediaQueryChange()
56
- }
57
-
58
- return () => {
59
- window.removeEventListener('pointerdown', handlePointerDown, { capture: true })
60
-
61
- if (mql) {
62
- mql.removeEventListener('change', handleMediaQueryChange)
63
- }
64
- }
65
- }, [editor])
10
+ // When the coarse pointer state changes, update the instance state
11
+ useReactor(
12
+ 'coarse pointer change',
13
+ () => {
14
+ const isCoarsePointer = tlenvReactive.get().isCoarsePointer
15
+ const isInstanceStateCoarsePointer = unsafe__withoutCapture(
16
+ () => editor.getInstanceState().isCoarsePointer
17
+ )
18
+ if (isCoarsePointer === isInstanceStateCoarsePointer) return
19
+ editor.updateInstanceState({ isCoarsePointer: isCoarsePointer })
20
+ },
21
+ [editor]
22
+ )
66
23
  }
@@ -27,7 +27,7 @@ import { useCallback, useDebugValue, useLayoutEffect, useRef } from 'react'
27
27
  export function useEvent<Args extends Array<unknown>, Result>(
28
28
  handler: (...args: Args) => Result
29
29
  ): (...args: Args) => Result {
30
- const handlerRef = useRef<(...args: Args) => Result>()
30
+ const handlerRef = useRef<((...args: Args) => Result) | undefined>(undefined)
31
31
 
32
32
  // In a real implementation, this would run before layout effects
33
33
  useLayoutEffect(() => {
@@ -9,7 +9,7 @@ const IGNORED_TAGS = ['textarea', 'input']
9
9
  * want this for drawing operations and can disable it by setting 'disableDoubleTapZoom' in the main
10
10
  * editor.
11
11
  */
12
- export function useFixSafariDoubleTapZoomPencilEvents(ref: React.RefObject<HTMLElement>) {
12
+ export function useFixSafariDoubleTapZoomPencilEvents(ref: React.RefObject<HTMLElement | null>) {
13
13
  const editor = useEditor()
14
14
 
15
15
  useEffect(() => {
@@ -75,7 +75,7 @@ const isWheelEndEvent = (time: number) => {
75
75
  return false
76
76
  }
77
77
 
78
- export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
78
+ export function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {
79
79
  const editor = useEditor()
80
80
 
81
81
  const events = React.useMemo(() => {
@@ -105,7 +105,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
105
105
  const util = editor.getShapeUtil(shape)
106
106
  if (util.canScroll(shape)) {
107
107
  const bounds = editor.getShapePageBounds(editingShapeId)
108
- if (bounds?.containsPoint(editor.inputs.currentPagePoint)) {
108
+ if (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) {
109
109
  return
110
110
  }
111
111
  }
@@ -4,7 +4,7 @@ import { useContainer } from './useContainer'
4
4
  import { useMaybeEditor } from './useEditor'
5
5
 
6
6
  /** @public */
7
- export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
7
+ export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement | null>) {
8
8
  if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
9
9
  const container = useContainer()
10
10
  const editor = useMaybeEditor()
@@ -4,7 +4,7 @@ import { useContainer } from './useContainer'
4
4
  import { useMaybeEditor } from './useEditor'
5
5
 
6
6
  /** @public */
7
- export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
7
+ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
8
8
  if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
9
9
  const container = useContainer()
10
10
  const editor = useMaybeEditor()
@@ -2,7 +2,7 @@ import { throttle } from '@tldraw/utils'
2
2
  import { useLayoutEffect } from 'react'
3
3
  import { useEditor } from './useEditor'
4
4
 
5
- export function useScreenBounds(ref: React.RefObject<HTMLElement>) {
5
+ export function useScreenBounds(ref: React.RefObject<HTMLElement | null>) {
6
6
  const editor = useEditor()
7
7
 
8
8
  useLayoutEffect(() => {
@@ -9,7 +9,10 @@ export function useStateAttribute() {
9
9
  // editor mounting and this attribute being applied, because styles may depend on it:
10
10
  useLayoutEffect(() => {
11
11
  return react('stateAttribute', () => {
12
- editor.getContainer().setAttribute('data-state', editor.getPath())
12
+ const container = editor.getContainer()
13
+ const instanceState = editor.getInstanceState()
14
+ container.setAttribute('data-state', editor.getPath())
15
+ container.setAttribute('data-coarse', String(instanceState.isCoarsePointer))
13
16
  })
14
17
  }, [editor])
15
18
  }
@@ -3,7 +3,7 @@ import { VecLike } from '../primitives/Vec'
3
3
 
4
4
  /** @public */
5
5
  export function useTransform(
6
- ref: React.RefObject<HTMLElement | SVGElement>,
6
+ ref: React.RefObject<HTMLElement | SVGElement | null>,
7
7
  x?: number,
8
8
  y?: number,
9
9
  scale?: number,
@@ -12,14 +12,9 @@ export function useZoomCss() {
12
12
  const setScale = (s: number) => container.style.setProperty('--tl-zoom', s.toString())
13
13
  const setScaleDebounced = debounce(setScale, 100)
14
14
 
15
- const scheduler = new EffectScheduler('useZoomCss', () => {
16
- const numShapes = editor.getCurrentPageShapeIds().size
17
- if (numShapes < 300) {
18
- setScale(editor.getZoomLevel())
19
- } else {
20
- setScaleDebounced(editor.getZoomLevel())
21
- }
22
- })
15
+ const scheduler = new EffectScheduler('useZoomCss', () =>
16
+ setScale(editor.getEfficientZoomLevel())
17
+ )
23
18
 
24
19
  scheduler.attach()
25
20
  scheduler.execute()
@@ -87,6 +87,33 @@ export interface TldrawOptions {
87
87
  * Branding name of the app, currently only used for adding aria-label for the application.
88
88
  */
89
89
  readonly branding?: string
90
+ /**
91
+ * Whether to use debounced zoom level for certain rendering optimizations. When true,
92
+ * `editor.getDebouncedZoomLevel()` returns a cached zoom value while the camera is moving,
93
+ * reducing re-renders. When false, it always returns the current zoom level.
94
+ */
95
+ readonly debouncedZoom: boolean
96
+ /**
97
+ * The number of shapes that must be on the page for the debounced zoom level to be used.
98
+ * Defaults to 300 shapes.
99
+ */
100
+ readonly debouncedZoomThreshold: number
101
+ /**
102
+ * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.
103
+ * When false, the spacebar will not pan the camera.
104
+ */
105
+ readonly spacebarPanning: boolean
106
+ /**
107
+ * The default padding (in pixels) used when zooming to fit content in the viewport.
108
+ * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.
109
+ * The actual padding used is the minimum of this value and 28% of the viewport width.
110
+ * Defaults to 128 pixels.
111
+ */
112
+ readonly zoomToFitPadding: number
113
+ /**
114
+ * The distance (in screen pixels) at which shapes snap to guides and other shapes.
115
+ */
116
+ readonly snapThreshold: number
90
117
  }
91
118
 
92
119
  /** @public */
@@ -139,4 +166,9 @@ export const defaultTldrawOptions = {
139
166
  enableToolbarKeyboardShortcuts: true,
140
167
  maxFontsToLoadBeforeRender: Infinity,
141
168
  nonce: undefined,
169
+ debouncedZoom: true,
170
+ debouncedZoomThreshold: 500,
171
+ spacebarPanning: true,
172
+ zoomToFitPadding: 128,
173
+ snapThreshold: 8,
142
174
  } as const satisfies TldrawOptions
@@ -180,6 +180,15 @@ export class Box {
180
180
  return new Vec(this.w, this.h)
181
181
  }
182
182
 
183
+ isValid() {
184
+ return (
185
+ Number.isFinite(this.x) &&
186
+ Number.isFinite(this.y) &&
187
+ Number.isFinite(this.w) &&
188
+ Number.isFinite(this.h)
189
+ )
190
+ }
191
+
183
192
  toFixed() {
184
193
  this.x = toPrecision(this.x)
185
194
  this.y = toPrecision(this.y)
@@ -140,6 +140,7 @@ export abstract class Geometry2d {
140
140
  }
141
141
  }
142
142
  if (!nearest) throw Error('nearest point not found')
143
+ dist = Math.sqrt(dist) // return the actual distance, not the squared distance
143
144
  return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
144
145
  }
145
146
 
@@ -32,7 +32,7 @@ export function getRotationSnapshot({
32
32
 
33
33
  return {
34
34
  initialPageCenter,
35
- initialCursorAngle: initialPageCenter.angle(editor.inputs.originPagePoint),
35
+ initialCursorAngle: initialPageCenter.angle(editor.inputs.getOriginPagePoint()),
36
36
  initialShapesRotation: rotation,
37
37
  shapeSnapshots: shapes.map((shape) => ({
38
38
  shape,
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '4.3.0-canary.c7096a59bf3b'
4
+ export const version = '4.3.0-canary.cb6779b4f066'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-12-01T14:44:15.569Z',
8
- patch: '2025-12-01T14:44:15.569Z',
7
+ minor: '2026-01-18T00:37:30.564Z',
8
+ patch: '2026-01-18T00:37:30.564Z',
9
9
  }