@tldraw/editor 3.14.0-canary.e0ab6f4c80f9 → 3.14.0-canary.e7ebd007cb8c

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 (215) hide show
  1. package/dist-cjs/index.d.ts +75 -70
  2. package/dist-cjs/index.js +10 -8
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +44 -73
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
  7. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  8. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -20
  9. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
  10. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
  11. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  13. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  14. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  15. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  16. package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +2 -0
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +5 -1
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  20. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
  21. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  25. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  26. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  27. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  29. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +8 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  31. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +6 -0
  32. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  34. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  35. package/dist-cjs/lib/primitives/Box.js +39 -33
  36. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  37. package/dist-cjs/lib/primitives/Vec.js +18 -13
  38. package/dist-cjs/lib/primitives/Vec.js.map +3 -3
  39. package/dist-cjs/lib/primitives/geometry/Arc2d.js +41 -21
  40. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Circle2d.js +11 -11
  42. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +13 -16
  44. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  45. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +4 -4
  46. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  47. package/dist-cjs/lib/primitives/geometry/Edge2d.js +14 -21
  48. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  49. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +10 -10
  50. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  51. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +5 -0
  52. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  53. package/dist-cjs/lib/primitives/geometry/Point2d.js +6 -6
  54. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  55. package/dist-cjs/lib/primitives/geometry/Polygon2d.js +3 -0
  56. package/dist-cjs/lib/primitives/geometry/Polygon2d.js.map +2 -2
  57. package/dist-cjs/lib/primitives/geometry/Polyline2d.js +8 -5
  58. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  59. package/dist-cjs/lib/primitives/geometry/Rectangle2d.js +22 -11
  60. package/dist-cjs/lib/primitives/geometry/Rectangle2d.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Stadium2d.js +22 -22
  62. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  63. package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
  64. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
  65. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  66. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  67. package/dist-cjs/lib/utils/richText.js.map +1 -1
  68. package/dist-cjs/version.js +3 -3
  69. package/dist-cjs/version.js.map +1 -1
  70. package/dist-esm/index.d.mts +75 -70
  71. package/dist-esm/index.mjs +17 -9
  72. package/dist-esm/index.mjs.map +2 -2
  73. package/dist-esm/lib/editor/Editor.mjs +44 -73
  74. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  75. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  76. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  77. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -20
  78. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
  79. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  80. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  81. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  82. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  83. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  84. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  85. package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +2 -0
  86. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  87. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +5 -1
  88. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  89. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
  90. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  91. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  92. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  93. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  94. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  95. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  96. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  97. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  98. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +8 -0
  99. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  100. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +6 -0
  101. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  102. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  103. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  104. package/dist-esm/lib/primitives/Box.mjs +39 -33
  105. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  106. package/dist-esm/lib/primitives/Vec.mjs +19 -14
  107. package/dist-esm/lib/primitives/Vec.mjs.map +3 -3
  108. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +41 -21
  109. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  110. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +11 -11
  111. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  112. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +13 -16
  113. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +4 -4
  115. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  116. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +14 -21
  117. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  118. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +11 -11
  119. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  120. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +7 -1
  121. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  122. package/dist-esm/lib/primitives/geometry/Point2d.mjs +6 -6
  123. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  124. package/dist-esm/lib/primitives/geometry/Polygon2d.mjs +3 -0
  125. package/dist-esm/lib/primitives/geometry/Polygon2d.mjs.map +2 -2
  126. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +8 -5
  127. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  128. package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs +22 -11
  129. package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs.map +2 -2
  130. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +22 -22
  131. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  132. package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
  133. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
  134. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  135. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  136. package/dist-esm/lib/utils/richText.mjs.map +1 -1
  137. package/dist-esm/version.mjs +3 -3
  138. package/dist-esm/version.mjs.map +1 -1
  139. package/package.json +7 -7
  140. package/src/index.ts +18 -8
  141. package/src/lib/editor/Editor.test.ts +252 -3
  142. package/src/lib/editor/Editor.ts +47 -75
  143. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  144. package/src/lib/editor/derivations/notVisibleShapes.ts +24 -25
  145. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  146. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  147. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  148. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  149. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  150. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  151. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +3 -1
  152. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  153. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +6 -2
  154. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  155. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
  156. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  157. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  158. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  159. package/src/lib/editor/managers/TextManager/TextManager.test.ts +411 -0
  160. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +1 -1
  161. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  162. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  163. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  164. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  165. package/src/lib/editor/shapes/ShapeUtil.ts +10 -1
  166. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +8 -0
  167. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +5 -2
  168. package/src/lib/exports/getSvgJsx.tsx +1 -1
  169. package/src/lib/primitives/Box.test.ts +588 -7
  170. package/src/lib/primitives/Box.ts +41 -33
  171. package/src/lib/primitives/Vec.test.ts +2 -2
  172. package/src/lib/primitives/Vec.ts +15 -10
  173. package/src/lib/primitives/geometry/Arc2d.ts +42 -23
  174. package/src/lib/primitives/geometry/Circle2d.ts +12 -12
  175. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +5 -0
  176. package/src/lib/primitives/geometry/CubicBezier2d.ts +13 -17
  177. package/src/lib/primitives/geometry/CubicSpline2d.ts +5 -5
  178. package/src/lib/primitives/geometry/Edge2d.ts +14 -25
  179. package/src/lib/primitives/geometry/Ellipse2d.ts +12 -13
  180. package/src/lib/primitives/geometry/Geometry2d.ts +6 -0
  181. package/src/lib/primitives/geometry/Point2d.ts +6 -6
  182. package/src/lib/primitives/geometry/Polygon2d.ts +4 -0
  183. package/src/lib/primitives/geometry/Polyline2d.ts +10 -7
  184. package/src/lib/primitives/geometry/Rectangle2d.ts +24 -11
  185. package/src/lib/primitives/geometry/Stadium2d.ts +22 -23
  186. package/src/lib/utils/areShapesContentEqual.ts +2 -1
  187. package/src/lib/utils/reorderShapes.ts +10 -13
  188. package/src/lib/utils/richText.ts +1 -1
  189. package/src/version.ts +3 -3
  190. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  191. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  192. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  193. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  194. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  195. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  196. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  197. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  198. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  199. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  200. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  201. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  202. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  203. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  204. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  205. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  206. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  207. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  208. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  209. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  210. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  211. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  212. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  213. package/src/lib/editor/managers/Stack.ts +0 -71
  214. /package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +0 -0
  215. /package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +0 -0
@@ -9,8 +9,7 @@ import {
9
9
  squashRecordDiffsMutable,
10
10
  } from '@tldraw/store'
11
11
  import { exhaustiveSwitchError, noop } from '@tldraw/utils'
12
- import { TLHistoryBatchOptions, TLHistoryEntry } from '../types/history-types'
13
- import { stack } from './Stack'
12
+ import { TLHistoryBatchOptions, TLHistoryEntry } from '../../types/history-types'
14
13
 
15
14
  enum HistoryRecorderState {
16
15
  Recording = 'recording',
@@ -349,3 +348,75 @@ class PendingDiff<R extends UnknownRecord> {
349
348
  return { diff: this.diff, isEmpty: this.isEmpty() }
350
349
  }
351
350
  }
351
+
352
+ import { EMPTY_ARRAY } from '@tldraw/state'
353
+
354
+ export type Stack<T> = StackItem<T> | EmptyStackItem<T>
355
+
356
+ export function stack<T>(items?: Array<T>): Stack<T> {
357
+ if (items) {
358
+ let result = EMPTY_STACK_ITEM as Stack<T>
359
+ while (items.length) {
360
+ result = result.push(items.pop()!)
361
+ }
362
+ return result
363
+ }
364
+ return EMPTY_STACK_ITEM as any
365
+ }
366
+
367
+ class EmptyStackItem<T> implements Iterable<T> {
368
+ readonly length = 0
369
+ readonly head = null
370
+ readonly tail: Stack<T> = this
371
+
372
+ push(head: T): Stack<T> {
373
+ return new StackItem<T>(head, this)
374
+ }
375
+
376
+ toArray() {
377
+ return EMPTY_ARRAY
378
+ }
379
+
380
+ [Symbol.iterator]() {
381
+ return {
382
+ next() {
383
+ return { value: undefined, done: true as const }
384
+ },
385
+ }
386
+ }
387
+ }
388
+
389
+ const EMPTY_STACK_ITEM = new EmptyStackItem()
390
+
391
+ class StackItem<T> implements Iterable<T> {
392
+ length: number
393
+ constructor(
394
+ public readonly head: T,
395
+ public readonly tail: Stack<T>
396
+ ) {
397
+ this.length = tail.length + 1
398
+ }
399
+
400
+ push(head: T): Stack<T> {
401
+ return new StackItem(head, this)
402
+ }
403
+
404
+ toArray() {
405
+ return Array.from(this)
406
+ }
407
+
408
+ [Symbol.iterator]() {
409
+ let stack = this as Stack<T>
410
+ return {
411
+ next() {
412
+ if (stack.length) {
413
+ const value = stack.head!
414
+ stack = stack.tail
415
+ return { value, done: false as const }
416
+ } else {
417
+ return { value: undefined, done: true as const }
418
+ }
419
+ },
420
+ }
421
+ }
422
+ }
@@ -0,0 +1,624 @@
1
+ import { TLScribble } from '@tldraw/tlschema'
2
+ import { Editor } from '../../Editor'
3
+ import { ScribbleItem, ScribbleManager } from './ScribbleManager'
4
+
5
+ // Mock the Editor class
6
+ jest.mock('../../Editor')
7
+ jest.mock('@tldraw/utils', () => ({
8
+ uniqueId: jest.fn(() => 'test-id'),
9
+ }))
10
+
11
+ describe('ScribbleManager', () => {
12
+ let editor: jest.Mocked<Editor>
13
+ let scribbleManager: ScribbleManager
14
+ let mockUniqueId: jest.Mock
15
+
16
+ beforeEach(() => {
17
+ editor = {
18
+ updateInstanceState: jest.fn(),
19
+ run: jest.fn((fn) => fn()),
20
+ } as any
21
+
22
+ const { uniqueId } = jest.requireMock('@tldraw/utils')
23
+ mockUniqueId = uniqueId
24
+ mockUniqueId.mockReturnValue('test-id')
25
+
26
+ scribbleManager = new ScribbleManager(editor)
27
+ })
28
+
29
+ afterEach(() => {
30
+ jest.clearAllMocks()
31
+ })
32
+
33
+ describe('constructor and initialization', () => {
34
+ it('should initialize with empty scribble items and paused state', () => {
35
+ expect(scribbleManager.scribbleItems.size).toBe(0)
36
+ expect(scribbleManager.state).toBe('paused')
37
+ })
38
+
39
+ it('should store reference to editor', () => {
40
+ expect((scribbleManager as any).editor).toBe(editor)
41
+ })
42
+ })
43
+
44
+ describe('addScribble', () => {
45
+ it('should add a new scribble with default values', () => {
46
+ const result = scribbleManager.addScribble({})
47
+
48
+ expect(result).toBeDefined()
49
+ expect(result.id).toBe('test-id')
50
+ expect(result.scribble).toMatchObject({
51
+ id: 'test-id',
52
+ size: 20,
53
+ color: 'accent',
54
+ opacity: 0.8,
55
+ delay: 0,
56
+ points: [],
57
+ shrink: 0.1,
58
+ taper: true,
59
+ state: 'starting',
60
+ })
61
+ expect(result.timeoutMs).toBe(0)
62
+ expect(result.delayRemaining).toBe(0)
63
+ expect(result.prev).toBeNull()
64
+ expect(result.next).toBeNull()
65
+ })
66
+
67
+ it('should add a scribble with custom properties', () => {
68
+ const customScribble: Partial<TLScribble> = {
69
+ size: 30,
70
+ color: 'black',
71
+ opacity: 0.5,
72
+ delay: 1000,
73
+ shrink: 0.2,
74
+ taper: false,
75
+ }
76
+
77
+ const result = scribbleManager.addScribble(customScribble)
78
+
79
+ expect(result.scribble).toMatchObject({
80
+ ...customScribble,
81
+ id: 'test-id',
82
+ points: [],
83
+ state: 'starting',
84
+ })
85
+ expect(result.delayRemaining).toBe(1000)
86
+ })
87
+
88
+ it('should add scribble with custom id', () => {
89
+ const customId = 'custom-scribble-id'
90
+ const result = scribbleManager.addScribble({}, customId)
91
+
92
+ expect(result.id).toBe(customId)
93
+ expect(result.scribble.id).toBe(customId)
94
+ expect(scribbleManager.scribbleItems.has(customId)).toBe(true)
95
+ })
96
+
97
+ it('should store scribble in scribbleItems map', () => {
98
+ const result = scribbleManager.addScribble({})
99
+
100
+ expect(scribbleManager.scribbleItems.size).toBe(1)
101
+ expect(scribbleManager.scribbleItems.get('test-id')).toBe(result)
102
+ })
103
+
104
+ it('should handle multiple scribbles', () => {
105
+ mockUniqueId.mockReturnValueOnce('id1').mockReturnValueOnce('id2').mockReturnValueOnce('id3')
106
+
107
+ const scribble1 = scribbleManager.addScribble({ color: 'black' })
108
+ const scribble2 = scribbleManager.addScribble({ color: 'white' })
109
+ const scribble3 = scribbleManager.addScribble({ color: 'accent' })
110
+
111
+ expect(scribbleManager.scribbleItems.size).toBe(3)
112
+ expect(scribble1.scribble.color).toBe('black')
113
+ expect(scribble2.scribble.color).toBe('white')
114
+ expect(scribble3.scribble.color).toBe('accent')
115
+ })
116
+ })
117
+
118
+ describe('reset', () => {
119
+ it('should clear all scribble items and update instance state', () => {
120
+ mockUniqueId.mockReturnValueOnce('id1').mockReturnValueOnce('id2')
121
+ scribbleManager.addScribble({})
122
+ scribbleManager.addScribble({})
123
+ expect(scribbleManager.scribbleItems.size).toBe(2)
124
+
125
+ scribbleManager.reset()
126
+
127
+ expect(scribbleManager.scribbleItems.size).toBe(0)
128
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({ scribbles: [] })
129
+ })
130
+
131
+ it('should work when no scribbles exist', () => {
132
+ expect(() => scribbleManager.reset()).not.toThrow()
133
+ expect(scribbleManager.scribbleItems.size).toBe(0)
134
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({ scribbles: [] })
135
+ })
136
+ })
137
+
138
+ describe('stop', () => {
139
+ it('should stop an existing scribble', () => {
140
+ const item = scribbleManager.addScribble({ delay: 1000 })
141
+ item.delayRemaining = 500
142
+
143
+ const result = scribbleManager.stop(item.id)
144
+
145
+ expect(result).toBe(item)
146
+ expect(result.scribble.state).toBe('stopping')
147
+ expect(result.delayRemaining).toBe(200) // min(500, 200)
148
+ })
149
+
150
+ it('should cap delay at 200ms when stopping', () => {
151
+ const item = scribbleManager.addScribble({ delay: 50 })
152
+ item.delayRemaining = 50
153
+
154
+ scribbleManager.stop(item.id)
155
+
156
+ expect(item.delayRemaining).toBe(50) // min(50, 200)
157
+ })
158
+
159
+ it('should throw error for non-existent scribble', () => {
160
+ expect(() => scribbleManager.stop('non-existent-id')).toThrow(
161
+ 'Scribble with id non-existent-id not found'
162
+ )
163
+ })
164
+
165
+ it('should handle stopping multiple scribbles', () => {
166
+ mockUniqueId.mockReturnValueOnce('id1').mockReturnValueOnce('id2')
167
+
168
+ const item1 = scribbleManager.addScribble({})
169
+ const item2 = scribbleManager.addScribble({})
170
+
171
+ scribbleManager.stop('id1')
172
+ scribbleManager.stop('id2')
173
+
174
+ expect(item1.scribble.state).toBe('stopping')
175
+ expect(item2.scribble.state).toBe('stopping')
176
+ })
177
+ })
178
+
179
+ describe('addPoint', () => {
180
+ it('should add point to existing scribble', () => {
181
+ const item = scribbleManager.addScribble({})
182
+
183
+ const result = scribbleManager.addPoint(item.id, 10, 20, 0.7)
184
+
185
+ expect(result).toBe(item)
186
+ expect(result.next).toEqual({ x: 10, y: 20, z: 0.7 })
187
+ })
188
+
189
+ it('should use default z value of 0.5', () => {
190
+ const item = scribbleManager.addScribble({})
191
+
192
+ scribbleManager.addPoint(item.id, 10, 20)
193
+
194
+ expect(item.next).toEqual({ x: 10, y: 20, z: 0.5 })
195
+ })
196
+
197
+ it('should only set next if distance from prev is >= 1', () => {
198
+ const item = scribbleManager.addScribble({})
199
+ item.prev = { x: 10, y: 20, z: 0.5 }
200
+
201
+ // Distance < 1 (should not set next)
202
+ scribbleManager.addPoint(item.id, 10.5, 20.3)
203
+ expect(item.next).toBeNull()
204
+
205
+ // Distance >= 1 (should set next)
206
+ scribbleManager.addPoint(item.id, 11, 21)
207
+ expect(item.next).toEqual({ x: 11, y: 21, z: 0.5 })
208
+ })
209
+
210
+ it('should set next when prev is null', () => {
211
+ const item = scribbleManager.addScribble({})
212
+ expect(item.prev).toBeNull()
213
+
214
+ scribbleManager.addPoint(item.id, 5, 5)
215
+
216
+ expect(item.next).toEqual({ x: 5, y: 5, z: 0.5 })
217
+ })
218
+
219
+ it('should throw error for non-existent scribble', () => {
220
+ expect(() => scribbleManager.addPoint('non-existent-id', 10, 20)).toThrow(
221
+ 'Scribble with id non-existent-id not found'
222
+ )
223
+ })
224
+
225
+ it('should handle multiple points', () => {
226
+ const item = scribbleManager.addScribble({})
227
+
228
+ scribbleManager.addPoint(item.id, 0, 0)
229
+ expect(item.next).toEqual({ x: 0, y: 0, z: 0.5 })
230
+
231
+ item.prev = item.next
232
+ scribbleManager.addPoint(item.id, 10, 10)
233
+ expect(item.next).toEqual({ x: 10, y: 10, z: 0.5 })
234
+ })
235
+ })
236
+
237
+ describe('tick', () => {
238
+ it('should return early when no scribble items exist', () => {
239
+ scribbleManager.tick(16)
240
+
241
+ expect(editor.run).not.toHaveBeenCalled()
242
+ })
243
+
244
+ it('should wrap tick operations in editor.run', () => {
245
+ scribbleManager.addScribble({})
246
+
247
+ scribbleManager.tick(16)
248
+
249
+ expect(editor.run).toHaveBeenCalledWith(expect.any(Function))
250
+ })
251
+
252
+ describe('starting state behavior', () => {
253
+ it('should add points to scribble in starting state', () => {
254
+ const item = scribbleManager.addScribble({})
255
+ item.next = { x: 10, y: 20, z: 0.5 }
256
+
257
+ scribbleManager.tick(16)
258
+
259
+ expect(item.prev).toEqual({ x: 10, y: 20, z: 0.5 })
260
+ expect(item.scribble.points).toHaveLength(1)
261
+ expect(item.scribble.points[0]).toEqual({ x: 10, y: 20, z: 0.5 })
262
+ })
263
+
264
+ it('should not add point if next equals prev', () => {
265
+ const item = scribbleManager.addScribble({})
266
+ const point = { x: 10, y: 20, z: 0.5 }
267
+ item.next = point
268
+ item.prev = point
269
+
270
+ scribbleManager.tick(16)
271
+
272
+ expect(item.scribble.points).toHaveLength(0)
273
+ })
274
+
275
+ it('should transition to active after 8 points', () => {
276
+ const item = scribbleManager.addScribble({})
277
+
278
+ // Add 9 points to trigger transition
279
+ for (let i = 0; i < 9; i++) {
280
+ item.next = { x: i, y: i, z: 0.5 }
281
+ item.prev = null // Reset prev to ensure point is added
282
+ scribbleManager.tick(16)
283
+ }
284
+
285
+ expect(item.scribble.state).toBe('active')
286
+ expect(item.scribble.points).toHaveLength(9)
287
+ })
288
+ })
289
+
290
+ describe('active state behavior', () => {
291
+ let item: ScribbleItem
292
+
293
+ beforeEach(() => {
294
+ item = scribbleManager.addScribble({})
295
+ item.scribble.state = 'active'
296
+ })
297
+
298
+ it('should add new points when next differs from prev', () => {
299
+ item.next = { x: 10, y: 20, z: 0.5 }
300
+ item.prev = { x: 0, y: 0, z: 0.5 }
301
+
302
+ scribbleManager.tick(16)
303
+
304
+ expect(item.prev).toEqual({ x: 10, y: 20, z: 0.5 })
305
+ expect(item.scribble.points).toContainEqual({ x: 10, y: 20, z: 0.5 })
306
+ })
307
+
308
+ it('should shrink from start when delay is finished and points > 8', () => {
309
+ // Set up scribble with > 8 points and no delay
310
+ for (let i = 0; i < 10; i++) {
311
+ item.scribble.points.push({ x: i, y: i, z: 0.5 })
312
+ }
313
+ item.delayRemaining = 0
314
+ item.next = { x: 50, y: 50, z: 0.5 }
315
+
316
+ scribbleManager.tick(16)
317
+
318
+ expect(item.scribble.points).toHaveLength(10) // Added one, removed one
319
+ expect(item.scribble.points[0]).toEqual({ x: 1, y: 1, z: 0.5 }) // First was removed
320
+ })
321
+
322
+ it('should shrink when not moving and timeout reached', () => {
323
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
324
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
325
+ item.timeoutMs = 16 // Will reset to 0, triggering shrink
326
+
327
+ scribbleManager.tick(16)
328
+
329
+ expect(item.scribble.points).toHaveLength(1)
330
+ expect(item.scribble.points[0]).toEqual({ x: 2, y: 2, z: 0.5 })
331
+ })
332
+
333
+ it('should reset delay when down to single point while stationary', () => {
334
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
335
+ item.scribble.delay = 500
336
+ item.delayRemaining = 0
337
+ item.timeoutMs = 16
338
+
339
+ scribbleManager.tick(16)
340
+
341
+ expect(item.delayRemaining).toBe(500)
342
+ })
343
+
344
+ it('should update timeout correctly', () => {
345
+ item.timeoutMs = 10
346
+
347
+ scribbleManager.tick(5)
348
+ expect(item.timeoutMs).toBe(15)
349
+
350
+ scribbleManager.tick(2)
351
+ expect(item.timeoutMs).toBe(0) // Reset when >= 16 (15 + 2 = 17)
352
+ })
353
+
354
+ it('should reduce delay remaining', () => {
355
+ item.delayRemaining = 100
356
+
357
+ scribbleManager.tick(30)
358
+
359
+ expect(item.delayRemaining).toBeLessThan(100)
360
+ })
361
+
362
+ it('should not reduce delay below 0', () => {
363
+ item.delayRemaining = 10
364
+
365
+ scribbleManager.tick(30)
366
+
367
+ expect(item.delayRemaining).toBe(0)
368
+ })
369
+ })
370
+
371
+ describe('stopping state behavior', () => {
372
+ let item: ScribbleItem
373
+
374
+ beforeEach(() => {
375
+ item = scribbleManager.addScribble({})
376
+ item.scribble.state = 'stopping'
377
+ })
378
+
379
+ it('should remove points when delay is finished and timeout reached', () => {
380
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
381
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
382
+ item.delayRemaining = 0
383
+ item.timeoutMs = 16
384
+
385
+ scribbleManager.tick(16)
386
+
387
+ expect(item.scribble.points).toHaveLength(1)
388
+ expect(item.scribble.points[0]).toEqual({ x: 2, y: 2, z: 0.5 })
389
+ })
390
+
391
+ it('should shrink scribble size when shrink is enabled', () => {
392
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
393
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
394
+ item.scribble.size = 20
395
+ item.scribble.shrink = 0.1
396
+ item.delayRemaining = 0
397
+ item.timeoutMs = 16
398
+
399
+ scribbleManager.tick(16)
400
+
401
+ expect(item.scribble.size).toBe(18) // 20 * (1 - 0.1)
402
+ })
403
+
404
+ it('should not shrink size below 1', () => {
405
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
406
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
407
+ item.scribble.size = 1.5
408
+ item.scribble.shrink = 0.8
409
+ item.delayRemaining = 0
410
+ item.timeoutMs = 16
411
+
412
+ scribbleManager.tick(16)
413
+
414
+ expect(item.scribble.size).toBe(1) // Math.max(1, 1.5 * 0.2)
415
+ })
416
+
417
+ it('should remove scribble when down to one point', () => {
418
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
419
+ item.delayRemaining = 0
420
+ item.timeoutMs = 16
421
+
422
+ scribbleManager.tick(16)
423
+
424
+ expect(scribbleManager.scribbleItems.has(item.id)).toBe(false)
425
+ })
426
+
427
+ it('should not process when delay remaining > 0', () => {
428
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
429
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
430
+ item.delayRemaining = 100
431
+ item.timeoutMs = 16
432
+
433
+ scribbleManager.tick(16)
434
+
435
+ expect(item.scribble.points).toHaveLength(2) // No change
436
+ })
437
+
438
+ it('should not process when timeout < 16', () => {
439
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
440
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
441
+ item.delayRemaining = 0
442
+ item.timeoutMs = 10
443
+
444
+ scribbleManager.tick(5)
445
+
446
+ expect(item.scribble.points).toHaveLength(2) // No change
447
+ expect(item.timeoutMs).toBe(15)
448
+ })
449
+ })
450
+
451
+ describe('paused state behavior', () => {
452
+ it('should do nothing when scribble is paused', () => {
453
+ const item = scribbleManager.addScribble({})
454
+ item.scribble.state = 'paused'
455
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
456
+ const originalPoints = [...item.scribble.points]
457
+
458
+ scribbleManager.tick(16)
459
+
460
+ expect(item.scribble.points).toEqual(originalPoints)
461
+ })
462
+ })
463
+
464
+ describe('instance state updates', () => {
465
+ it('should update instance state with scribbles', () => {
466
+ mockUniqueId.mockReturnValueOnce('id1').mockReturnValueOnce('id2')
467
+ scribbleManager.addScribble({ color: 'black' })
468
+ scribbleManager.addScribble({ color: 'white' })
469
+
470
+ scribbleManager.tick(16)
471
+
472
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({
473
+ scribbles: expect.arrayContaining([
474
+ expect.objectContaining({
475
+ color: 'black',
476
+ points: expect.any(Array),
477
+ }),
478
+ expect.objectContaining({
479
+ color: 'white',
480
+ points: expect.any(Array),
481
+ }),
482
+ ]),
483
+ })
484
+ })
485
+
486
+ it('should create copies of scribbles for instance state', () => {
487
+ const item = scribbleManager.addScribble({})
488
+ item.scribble.points.push({ x: 1, y: 1, z: 0.5 })
489
+
490
+ scribbleManager.tick(16)
491
+
492
+ const call = editor.updateInstanceState.mock.calls[0][0]
493
+ const scribbleInState = call.scribbles![0]
494
+
495
+ // Modify the original
496
+ item.scribble.points.push({ x: 2, y: 2, z: 0.5 })
497
+
498
+ // State copy should be unaffected
499
+ expect(scribbleInState.points).toHaveLength(1)
500
+ })
501
+
502
+ it('should limit scribbles to 5 items', () => {
503
+ // Add 7 scribbles
504
+ const colors = ['accent', 'black', 'white', 'laser', 'muted-1', 'accent', 'black'] as const
505
+ for (let i = 0; i < 7; i++) {
506
+ mockUniqueId.mockReturnValueOnce(`id${i}`)
507
+ scribbleManager.addScribble({ color: colors[i] })
508
+ }
509
+
510
+ scribbleManager.tick(16)
511
+
512
+ const call = editor.updateInstanceState.mock.calls[0][0]
513
+ expect(call.scribbles).toHaveLength(5)
514
+ })
515
+ })
516
+ })
517
+
518
+ describe('edge cases and error handling', () => {
519
+ it('should handle Vec.Dist calculation edge cases', () => {
520
+ const item = scribbleManager.addScribble({})
521
+ item.prev = { x: 0, y: 0, z: 0 }
522
+
523
+ // Exactly distance 1
524
+ scribbleManager.addPoint(item.id, 1, 0)
525
+ expect(item.next).toEqual({ x: 1, y: 0, z: 0.5 })
526
+
527
+ // Reset and test just under distance 1
528
+ item.next = null
529
+ scribbleManager.addPoint(item.id, 0.9, 0)
530
+ expect(item.next).toBeNull()
531
+ })
532
+
533
+ it('should handle multiple scribbles in different states', () => {
534
+ mockUniqueId
535
+ .mockReturnValueOnce('starting')
536
+ .mockReturnValueOnce('active')
537
+ .mockReturnValueOnce('stopping')
538
+
539
+ const startingItem = scribbleManager.addScribble({})
540
+ const activeItem = scribbleManager.addScribble({})
541
+ const stoppingItem = scribbleManager.addScribble({})
542
+
543
+ activeItem.scribble.state = 'active'
544
+ stoppingItem.scribble.state = 'stopping'
545
+
546
+ startingItem.next = { x: 1, y: 1, z: 0.5 }
547
+ activeItem.next = { x: 2, y: 2, z: 0.5 }
548
+ stoppingItem.scribble.points.push({ x: 3, y: 3, z: 0.5 })
549
+ stoppingItem.delayRemaining = 0
550
+ stoppingItem.timeoutMs = 16
551
+
552
+ scribbleManager.tick(16)
553
+
554
+ expect(startingItem.scribble.points).toHaveLength(1)
555
+ expect(activeItem.scribble.points).toHaveLength(1)
556
+ expect(scribbleManager.scribbleItems.has('stopping')).toBe(false) // Removed
557
+ })
558
+
559
+ it('should handle tick with 0 elapsed time', () => {
560
+ const item = scribbleManager.addScribble({})
561
+ item.delayRemaining = 100
562
+
563
+ expect(() => scribbleManager.tick(0)).not.toThrow()
564
+ expect(item.delayRemaining).toBe(100) // Should remain unchanged
565
+ })
566
+
567
+ it('should handle negative elapsed time', () => {
568
+ const item = scribbleManager.addScribble({})
569
+ item.delayRemaining = 100
570
+
571
+ scribbleManager.tick(-50)
572
+
573
+ expect(item.delayRemaining).toBe(100) // Should remain unchanged or handle gracefully
574
+ })
575
+
576
+ it('should handle empty points array operations', () => {
577
+ const item = scribbleManager.addScribble({})
578
+ item.scribble.state = 'active'
579
+ item.timeoutMs = 16
580
+
581
+ expect(() => scribbleManager.tick(16)).not.toThrow()
582
+ })
583
+ })
584
+
585
+ describe('integration scenarios', () => {
586
+ it('should handle complete scribble lifecycle', () => {
587
+ const item = scribbleManager.addScribble({ delay: 100 })
588
+
589
+ // Starting state - add points
590
+ for (let i = 0; i < 10; i++) {
591
+ item.next = { x: i, y: i, z: 0.5 }
592
+ item.prev = null
593
+ scribbleManager.tick(16)
594
+ }
595
+
596
+ expect(item.scribble.state).toBe('active')
597
+ expect(item.scribble.points).toHaveLength(10)
598
+
599
+ // Stop the scribble
600
+ scribbleManager.stop(item.id)
601
+ expect(item.scribble.state).toBe('stopping')
602
+
603
+ // Process until removed
604
+ let iterations = 0
605
+ while (scribbleManager.scribbleItems.has(item.id) && iterations < 20) {
606
+ scribbleManager.tick(16)
607
+ iterations++
608
+ }
609
+
610
+ expect(scribbleManager.scribbleItems.has(item.id)).toBe(false)
611
+ })
612
+
613
+ it('should handle rapid point additions', () => {
614
+ const item = scribbleManager.addScribble({})
615
+
616
+ // Add many points rapidly
617
+ for (let i = 0; i < 100; i++) {
618
+ scribbleManager.addPoint(item.id, i * 2, i * 2) // Ensure distance > 1
619
+ }
620
+
621
+ expect(item.next).toEqual({ x: 198, y: 198, z: 0.5 })
622
+ })
623
+ })
624
+ })
@@ -1,7 +1,7 @@
1
1
  import { TLScribble, VecModel } from '@tldraw/tlschema'
2
2
  import { uniqueId } from '@tldraw/utils'
3
- import { Vec } from '../../primitives/Vec'
4
- import { Editor } from '../Editor'
3
+ import { Vec } from '../../../primitives/Vec'
4
+ import { Editor } from '../../Editor'
5
5
 
6
6
  /** @public */
7
7
  export interface ScribbleItem {