@tldraw/editor 4.3.0 → 4.4.0-canary.09e80a09d230

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