@tldraw/editor 4.3.0-next.7f179bd04d6c → 4.3.0-next.842fb21476f2

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 (165) hide show
  1. package/dist-cjs/index.d.ts +441 -120
  2. package/dist-cjs/index.js +6 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
  5. package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
  6. package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +3 -3
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/constants.js +1 -3
  10. package/dist-cjs/lib/constants.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +288 -275
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +18 -17
  14. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  15. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
  16. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
  18. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
  20. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
  22. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
  23. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
  24. package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
  26. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
  28. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
  30. package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
  31. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
  32. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  33. package/dist-cjs/lib/exports/parseCss.js +1 -1
  34. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  35. package/dist-cjs/lib/globals/environment.js +45 -9
  36. package/dist-cjs/lib/globals/environment.js.map +2 -2
  37. package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
  38. package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useEvent.js +1 -1
  40. package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
  41. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  43. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  44. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  45. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
  48. package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
  49. package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
  50. package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
  51. package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
  52. package/dist-cjs/lib/options.js +6 -1
  53. package/dist-cjs/lib/options.js.map +2 -2
  54. package/dist-cjs/lib/primitives/Box.js +3 -0
  55. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  56. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
  57. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  58. package/dist-cjs/lib/utils/rotation.js +1 -1
  59. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  60. package/dist-cjs/version.js +3 -3
  61. package/dist-cjs/version.js.map +1 -1
  62. package/dist-esm/index.d.mts +441 -120
  63. package/dist-esm/index.mjs +7 -2
  64. package/dist-esm/index.mjs.map +2 -2
  65. package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
  66. package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
  67. package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +3 -3
  69. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  70. package/dist-esm/lib/constants.mjs +1 -3
  71. package/dist-esm/lib/constants.mjs.map +2 -2
  72. package/dist-esm/lib/editor/Editor.mjs +289 -278
  73. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  74. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +18 -17
  75. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  76. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
  77. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
  79. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
  81. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
  83. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
  84. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
  85. package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
  87. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  88. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
  89. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  90. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
  91. package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
  92. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
  93. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  94. package/dist-esm/lib/exports/parseCss.mjs +1 -1
  95. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  96. package/dist-esm/lib/globals/environment.mjs +45 -9
  97. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  98. package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
  99. package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
  100. package/dist-esm/lib/hooks/useEvent.mjs +1 -1
  101. package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
  102. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  103. package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
  104. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  105. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  106. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  107. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  108. package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
  109. package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
  110. package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
  111. package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
  112. package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
  113. package/dist-esm/lib/options.mjs +6 -1
  114. package/dist-esm/lib/options.mjs.map +2 -2
  115. package/dist-esm/lib/primitives/Box.mjs +3 -0
  116. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  117. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
  118. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  119. package/dist-esm/lib/utils/rotation.mjs +1 -1
  120. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  121. package/dist-esm/version.mjs +3 -3
  122. package/dist-esm/version.mjs.map +1 -1
  123. package/editor.css +14 -12
  124. package/package.json +18 -16
  125. package/src/index.ts +4 -1
  126. package/src/lib/components/ErrorBoundary.tsx +1 -1
  127. package/src/lib/components/GeometryDebuggingView.tsx +1 -19
  128. package/src/lib/components/default-components/DefaultCanvas.tsx +3 -3
  129. package/src/lib/config/TLUserPreferences.test.ts +40 -0
  130. package/src/lib/constants.ts +0 -2
  131. package/src/lib/editor/Editor.test.ts +140 -0
  132. package/src/lib/editor/Editor.ts +374 -321
  133. package/src/lib/editor/derivations/notVisibleShapes.ts +37 -23
  134. package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
  135. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
  136. package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
  137. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
  138. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
  139. package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
  140. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
  141. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
  142. package/src/lib/editor/managers/SnapManager/SnapManager.ts +1 -1
  143. package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
  144. package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
  145. package/src/lib/editor/shapes/ShapeUtil.ts +67 -24
  146. package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
  147. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  148. package/src/lib/exports/parseCss.test.ts +1 -0
  149. package/src/lib/exports/parseCss.ts +1 -1
  150. package/src/lib/globals/environment.ts +65 -10
  151. package/src/lib/hooks/useCoarsePointer.ts +16 -59
  152. package/src/lib/hooks/useEvent.tsx +1 -1
  153. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  154. package/src/lib/hooks/useGestureEvents.ts +2 -2
  155. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
  156. package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
  157. package/src/lib/hooks/useScreenBounds.ts +1 -1
  158. package/src/lib/hooks/useStateAttribute.ts +4 -1
  159. package/src/lib/hooks/useTransform.ts +1 -1
  160. package/src/lib/hooks/useZoomCss.ts +3 -8
  161. package/src/lib/options.ts +32 -0
  162. package/src/lib/primitives/Box.ts +9 -0
  163. package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
  164. package/src/lib/utils/rotation.ts +1 -1
  165. package/src/version.ts +3 -3
@@ -21,6 +21,17 @@ describe('TickManager', () => {
21
21
  let tickManager: TickManager
22
22
  let mockEmit: Mock
23
23
  let mockDisposablesAdd: Mock
24
+ let mockInputs: {
25
+ _currentScreenPoint: Vec
26
+ getCurrentScreenPoint(): Vec
27
+ currentScreenPoint: Vec
28
+ setCurrentScreenPoint(value: Vec): void
29
+ _pointerVelocity: Vec
30
+ getPointerVelocity(): Vec
31
+ pointerVelocity: Vec
32
+ setPointerVelocity(value: Vec): void
33
+ updatePointerVelocity(elapsed: number): void
34
+ }
24
35
 
25
36
  beforeEach(() => {
26
37
  vi.clearAllMocks()
@@ -41,16 +52,40 @@ describe('TickManager', () => {
41
52
  mockEmit = vi.fn()
42
53
  mockDisposablesAdd = vi.fn()
43
54
 
55
+ // Create a mock inputs object with getters and setters
56
+ mockInputs = {
57
+ _currentScreenPoint: new Vec(100, 100),
58
+ getCurrentScreenPoint() {
59
+ return this._currentScreenPoint
60
+ },
61
+ get currentScreenPoint() {
62
+ return this.getCurrentScreenPoint()
63
+ },
64
+ setCurrentScreenPoint(value: Vec) {
65
+ this._currentScreenPoint = value
66
+ },
67
+ _pointerVelocity: new Vec(0, 0),
68
+ getPointerVelocity() {
69
+ return this._pointerVelocity
70
+ },
71
+ get pointerVelocity() {
72
+ return this.getPointerVelocity()
73
+ },
74
+ setPointerVelocity(value: Vec) {
75
+ this._pointerVelocity = value
76
+ },
77
+ updatePointerVelocity(_elapsed: number) {
78
+ // Mock implementation - no-op for tests
79
+ },
80
+ }
81
+
44
82
  editor = {
45
83
  emit: mockEmit,
46
84
  disposables: {
47
85
  add: mockDisposablesAdd,
48
86
  },
49
- inputs: {
50
- currentScreenPoint: new Vec(100, 100),
51
- pointerVelocity: new Vec(0, 0),
52
- },
53
- } as any
87
+ inputs: mockInputs as unknown as Editor['inputs'],
88
+ } as unknown as Mocked<Editor>
54
89
 
55
90
  tickManager = new TickManager(editor)
56
91
  })
@@ -143,16 +178,6 @@ describe('TickManager', () => {
143
178
  expect(mockEmit).toHaveBeenCalledTimes(2)
144
179
  })
145
180
 
146
- it('should update pointer velocity', () => {
147
- const updatePointerVelocitySpy = vi.spyOn(tickManager as any, 'updatePointerVelocity')
148
- tickManager.now = 1000
149
- mockDateNow.mockReturnValue(1016)
150
-
151
- tickManager.tick()
152
-
153
- expect(updatePointerVelocitySpy).toHaveBeenCalledWith(16)
154
- })
155
-
156
181
  it('should schedule next RAF', () => {
157
182
  tickManager.tick()
158
183
  expect(mockRequestAnimationFrame).toHaveBeenCalled()
@@ -192,98 +217,6 @@ describe('TickManager', () => {
192
217
  })
193
218
  })
194
219
 
195
- describe('updatePointerVelocity method', () => {
196
- let updatePointerVelocity: any
197
-
198
- beforeEach(() => {
199
- // Access private method for testing
200
- updatePointerVelocity = (tickManager as any).updatePointerVelocity.bind(tickManager)
201
- // Reset the prevPoint to a known state
202
- ;(tickManager as any).prevPoint = new Vec(50, 50)
203
- })
204
-
205
- it('should return early if elapsed time is 0', () => {
206
- const originalVelocity = editor.inputs.pointerVelocity.clone()
207
-
208
- updatePointerVelocity(0)
209
-
210
- expect(editor.inputs.pointerVelocity).toEqual(originalVelocity)
211
- })
212
-
213
- it('should calculate velocity based on pointer movement', () => {
214
- editor.inputs.currentScreenPoint = new Vec(150, 150)
215
- ;(tickManager as any).prevPoint = new Vec(50, 50)
216
- editor.inputs.pointerVelocity = new Vec(0, 0)
217
-
218
- updatePointerVelocity(100) // 100ms elapsed
219
-
220
- // Delta should be (100, 100), length = ~141.42, direction = (~0.707, ~0.707)
221
- // Velocity should be length/elapsed = ~1.414 per ms in each direction
222
- // But with linear interpolation (lrp factor 0.5), it will be halved
223
- const expectedVelocity = new Vec(0.5, 0.5) // With lrp factor 0.5
224
- expect(editor.inputs.pointerVelocity.x).toBeCloseTo(expectedVelocity.x, 1)
225
- expect(editor.inputs.pointerVelocity.y).toBeCloseTo(expectedVelocity.y, 1)
226
- })
227
-
228
- it('should update prevPoint to current screen point', () => {
229
- const newPoint = new Vec(200, 200)
230
- editor.inputs.currentScreenPoint = newPoint
231
-
232
- updatePointerVelocity(16)
233
-
234
- expect((tickManager as any).prevPoint).toEqual(newPoint)
235
- })
236
-
237
- it('should use linear interpolation to smooth velocity', () => {
238
- editor.inputs.currentScreenPoint = new Vec(150, 50)
239
- ;(tickManager as any).prevPoint = new Vec(50, 50)
240
- editor.inputs.pointerVelocity = new Vec(2, 2)
241
-
242
- updatePointerVelocity(100)
243
-
244
- // Should interpolate between current velocity (2,2) and new velocity (1,0)
245
- // lrp with factor 0.5 should give us something between them
246
- expect(editor.inputs.pointerVelocity.x).toBeGreaterThan(0.5)
247
- expect(editor.inputs.pointerVelocity.x).toBeLessThan(2)
248
- })
249
-
250
- it('should set very small velocity components to 0', () => {
251
- editor.inputs.currentScreenPoint = new Vec(50.005, 50.005)
252
- ;(tickManager as any).prevPoint = new Vec(50, 50)
253
- editor.inputs.pointerVelocity = new Vec(0, 0)
254
-
255
- updatePointerVelocity(1000) // Long elapsed time = very small velocity
256
-
257
- expect(editor.inputs.pointerVelocity.x).toBe(0)
258
- expect(editor.inputs.pointerVelocity.y).toBe(0)
259
- })
260
-
261
- it('should handle zero movement (stationary pointer)', () => {
262
- editor.inputs.currentScreenPoint = new Vec(100, 100)
263
- ;(tickManager as any).prevPoint = new Vec(100, 100)
264
- editor.inputs.pointerVelocity = new Vec(1, 1)
265
-
266
- updatePointerVelocity(16)
267
-
268
- // Should interpolate towards zero velocity
269
- expect(editor.inputs.pointerVelocity.x).toBeLessThan(1)
270
- expect(editor.inputs.pointerVelocity.y).toBeLessThan(1)
271
- })
272
-
273
- it('should only update pointerVelocity if it actually changes', () => {
274
- // Setup scenario where velocity won't change
275
- editor.inputs.currentScreenPoint = new Vec(100, 100)
276
- ;(tickManager as any).prevPoint = new Vec(100, 100)
277
- editor.inputs.pointerVelocity = new Vec(0, 0)
278
-
279
- const originalVelocity = editor.inputs.pointerVelocity
280
- updatePointerVelocity(16)
281
-
282
- // Should still be the same object reference since no change occurred
283
- expect(editor.inputs.pointerVelocity).toBe(originalVelocity)
284
- })
285
- })
286
-
287
220
  describe('RAF throttling behavior', () => {
288
221
  it('should use test-specific RAF behavior in test environment', () => {
289
222
  // The TickManager should detect test environment and use requestAnimationFrame directly
@@ -1,5 +1,4 @@
1
1
  import { throttleToNextFrame as _throttleToNextFrame, bind } from '@tldraw/utils'
2
- import { Vec } from '../../../primitives/Vec'
3
2
  import { Editor } from '../../Editor'
4
3
 
5
4
  const throttleToNextFrame =
@@ -13,6 +12,7 @@ const throttleToNextFrame =
13
12
  }
14
13
  : _throttleToNextFrame
15
14
 
15
+ /** @internal */
16
16
  export class TickManager {
17
17
  constructor(public editor: Editor) {
18
18
  this.editor.disposables.add(this.dispose)
@@ -40,7 +40,7 @@ export class TickManager {
40
40
  const elapsed = now - this.now
41
41
  this.now = now
42
42
 
43
- this.updatePointerVelocity(elapsed)
43
+ this.editor.inputs.updatePointerVelocity(elapsed)
44
44
  this.editor.emit('frame', elapsed)
45
45
  this.editor.emit('tick', elapsed)
46
46
  this.cancelRaf = throttleToNextFrame(this.tick)
@@ -53,34 +53,4 @@ export class TickManager {
53
53
 
54
54
  this.cancelRaf?.()
55
55
  }
56
-
57
- private prevPoint = new Vec()
58
-
59
- updatePointerVelocity(elapsed: number) {
60
- const {
61
- prevPoint,
62
- editor: {
63
- inputs: { currentScreenPoint, pointerVelocity },
64
- },
65
- } = this
66
-
67
- if (elapsed === 0) return
68
-
69
- const delta = Vec.Sub(currentScreenPoint, prevPoint)
70
- this.prevPoint = currentScreenPoint.clone()
71
-
72
- const length = delta.len()
73
- const direction = length ? delta.div(length) : new Vec(0, 0)
74
-
75
- // consider adjusting this with an easing rather than a linear interpolation
76
- const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), 0.5)
77
-
78
- // if the velocity is very small, just set it to 0
79
- if (Math.abs(next.x) < 0.01) next.x = 0
80
- if (Math.abs(next.y) < 0.01) next.y = 0
81
-
82
- if (!pointerVelocity.equals(next)) {
83
- this.editor.inputs.pointerVelocity = next
84
- }
85
- }
86
56
  }
@@ -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 }