@tldraw/editor 3.14.0-canary.f6a0206007b3 → 3.14.0-canary.fd17def565a1

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 (128) hide show
  1. package/dist-cjs/index.d.ts +11 -21
  2. package/dist-cjs/index.js +8 -8
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +42 -71
  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/parentsToChildren.js +16 -16
  9. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  11. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  12. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  13. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  14. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +5 -1
  16. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
  18. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  19. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  20. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  25. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  28. package/dist-cjs/lib/primitives/Box.js +33 -33
  29. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  30. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  31. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  32. package/dist-cjs/lib/utils/richText.js.map +1 -1
  33. package/dist-cjs/version.js +3 -3
  34. package/dist-cjs/version.js.map +1 -1
  35. package/dist-esm/index.d.mts +11 -21
  36. package/dist-esm/index.mjs +12 -8
  37. package/dist-esm/index.mjs.map +2 -2
  38. package/dist-esm/lib/editor/Editor.mjs +42 -71
  39. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  40. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  41. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  42. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  43. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  44. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  45. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  46. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  47. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  48. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  49. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +5 -1
  50. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  51. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
  52. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  53. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  54. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  55. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  56. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  57. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  58. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  59. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  60. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  61. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  62. package/dist-esm/lib/primitives/Box.mjs +33 -33
  63. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  64. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  65. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  66. package/dist-esm/lib/utils/richText.mjs.map +1 -1
  67. package/dist-esm/version.mjs +3 -3
  68. package/dist-esm/version.mjs.map +1 -1
  69. package/package.json +7 -7
  70. package/src/index.ts +13 -7
  71. package/src/lib/editor/Editor.test.ts +252 -3
  72. package/src/lib/editor/Editor.ts +44 -73
  73. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  74. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  75. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  76. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  77. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  78. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  79. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  80. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
  81. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  82. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +6 -2
  83. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  84. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
  85. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  86. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  87. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  88. package/src/lib/editor/managers/TextManager/TextManager.test.ts +411 -0
  89. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +1 -1
  90. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  91. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  92. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  93. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  94. package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
  95. package/src/lib/exports/getSvgJsx.tsx +1 -1
  96. package/src/lib/primitives/Box.test.ts +588 -7
  97. package/src/lib/primitives/Box.ts +33 -33
  98. package/src/lib/utils/reorderShapes.ts +10 -13
  99. package/src/lib/utils/richText.ts +1 -1
  100. package/src/version.ts +3 -3
  101. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  102. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  103. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  104. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  105. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  106. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  107. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  108. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  109. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  110. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  111. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  112. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  113. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  114. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  115. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  116. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  117. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  118. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  119. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  120. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  121. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  122. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  123. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  124. package/src/lib/editor/managers/Stack.ts +0 -71
  125. /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
  126. /package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +0 -0
  127. /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
  128. /package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +0 -0
@@ -0,0 +1,485 @@
1
+ import { TLFrameShape, TLGroupShape, TLPageId, TLShapeId, createShapeId } from '@tldraw/tlschema'
2
+ import { Box } from '../../../primitives/Box'
3
+ import { Vec } from '../../../primitives/Vec'
4
+ import { Editor } from '../../Editor'
5
+ import { BoundsSnaps } from './BoundsSnaps'
6
+ import { HandleSnaps } from './HandleSnaps'
7
+ import { GapsSnapIndicator, PointsSnapIndicator, SnapManager } from './SnapManager'
8
+
9
+ // Mock the Editor class
10
+ jest.mock('../../Editor')
11
+ jest.mock('./BoundsSnaps')
12
+ jest.mock('./HandleSnaps')
13
+
14
+ describe('SnapManager', () => {
15
+ let editor: jest.Mocked<Editor>
16
+ let snapManager: SnapManager
17
+
18
+ const createMockShape = (
19
+ id: TLShapeId,
20
+ type: string = 'geo',
21
+ parentId: TLShapeId | string = 'page:page'
22
+ ) => ({
23
+ id,
24
+ type,
25
+ parentId,
26
+ x: 0,
27
+ y: 0,
28
+ rotation: 0,
29
+ index: 'a1' as const,
30
+ opacity: 1,
31
+ isLocked: false,
32
+ meta: {},
33
+ props: {},
34
+ typeName: 'shape' as const,
35
+ })
36
+
37
+ const createMockFrameShape = (id: TLShapeId): TLFrameShape =>
38
+ ({
39
+ ...createMockShape(id, 'frame'),
40
+ type: 'frame',
41
+ props: {
42
+ w: 100,
43
+ h: 100,
44
+ name: '',
45
+ },
46
+ }) as TLFrameShape
47
+
48
+ const createMockGroupShape = (id: TLShapeId): TLGroupShape =>
49
+ ({
50
+ ...createMockShape(id, 'group'),
51
+ type: 'group',
52
+ props: {},
53
+ }) as TLGroupShape
54
+
55
+ beforeEach(() => {
56
+ editor = {
57
+ getZoomLevel: jest.fn(() => 1),
58
+ getViewportPageBounds: jest.fn(() => new Box(0, 0, 1000, 1000)),
59
+ getSelectedShapeIds: jest.fn(() => []),
60
+ getSelectedShapes: jest.fn(() => []),
61
+ findCommonAncestor: jest.fn(() => createShapeId('page')),
62
+ getCurrentPageId: jest.fn(() => 'page:page' as TLPageId),
63
+ getSortedChildIdsForParent: jest.fn(() => []),
64
+ getShape: jest.fn(),
65
+ getShapeUtil: jest.fn(() => ({
66
+ canSnap: jest.fn(() => true),
67
+ })),
68
+ getShapePageBounds: jest.fn(),
69
+ isShapeOfType: jest.fn(),
70
+ } as any
71
+
72
+ snapManager = new SnapManager(editor)
73
+ })
74
+
75
+ afterEach(() => {
76
+ jest.clearAllMocks()
77
+ })
78
+
79
+ describe('constructor and initialization', () => {
80
+ it('should initialize with editor reference', () => {
81
+ expect(snapManager.editor).toBe(editor)
82
+ })
83
+
84
+ it('should create BoundsSnaps instance', () => {
85
+ expect(BoundsSnaps).toHaveBeenCalledWith(snapManager)
86
+ expect(snapManager.shapeBounds).toBeInstanceOf(BoundsSnaps)
87
+ })
88
+
89
+ it('should create HandleSnaps instance', () => {
90
+ expect(HandleSnaps).toHaveBeenCalledWith(snapManager)
91
+ expect(snapManager.handles).toBeInstanceOf(HandleSnaps)
92
+ })
93
+
94
+ it('should initialize snap indicators as undefined', () => {
95
+ expect(snapManager.getIndicators()).toEqual([])
96
+ })
97
+ })
98
+
99
+ describe('indicator management', () => {
100
+ describe('getIndicators', () => {
101
+ it('should return empty array when no indicators are set', () => {
102
+ expect(snapManager.getIndicators()).toEqual([])
103
+ })
104
+
105
+ it('should return set indicators', () => {
106
+ const indicators: PointsSnapIndicator[] = [
107
+ {
108
+ id: 'test-indicator',
109
+ type: 'points',
110
+ points: [
111
+ { x: 10, y: 20 },
112
+ { x: 30, y: 40 },
113
+ ],
114
+ },
115
+ ]
116
+
117
+ snapManager.setIndicators(indicators)
118
+ expect(snapManager.getIndicators()).toEqual(indicators)
119
+ })
120
+ })
121
+
122
+ describe('setIndicators', () => {
123
+ it('should set points indicators', () => {
124
+ const indicators: PointsSnapIndicator[] = [
125
+ {
126
+ id: 'points-indicator',
127
+ type: 'points',
128
+ points: [new Vec(10, 20), new Vec(30, 40)],
129
+ },
130
+ ]
131
+
132
+ snapManager.setIndicators(indicators)
133
+ expect(snapManager.getIndicators()).toEqual(indicators)
134
+ })
135
+
136
+ it('should set gaps indicators', () => {
137
+ const indicators: GapsSnapIndicator[] = [
138
+ {
139
+ id: 'gaps-indicator',
140
+ type: 'gaps',
141
+ direction: 'horizontal',
142
+ gaps: [
143
+ {
144
+ startEdge: [
145
+ { x: 0, y: 0 },
146
+ { x: 10, y: 0 },
147
+ ],
148
+ endEdge: [
149
+ { x: 20, y: 0 },
150
+ { x: 30, y: 0 },
151
+ ],
152
+ },
153
+ ],
154
+ },
155
+ ]
156
+
157
+ snapManager.setIndicators(indicators)
158
+ expect(snapManager.getIndicators()).toEqual(indicators)
159
+ })
160
+
161
+ it('should set mixed indicator types', () => {
162
+ const indicators = [
163
+ {
164
+ id: 'points-indicator',
165
+ type: 'points' as const,
166
+ points: [{ x: 10, y: 20 }],
167
+ },
168
+ {
169
+ id: 'gaps-indicator',
170
+ type: 'gaps' as const,
171
+ direction: 'vertical' as const,
172
+ gaps: [
173
+ {
174
+ startEdge: [
175
+ { x: 0, y: 0 },
176
+ { x: 0, y: 10 },
177
+ ] as [Vec, Vec],
178
+ endEdge: [
179
+ { x: 0, y: 20 },
180
+ { x: 0, y: 30 },
181
+ ] as [Vec, Vec],
182
+ },
183
+ ],
184
+ },
185
+ ]
186
+
187
+ snapManager.setIndicators(indicators)
188
+ expect(snapManager.getIndicators()).toEqual(indicators)
189
+ })
190
+ })
191
+
192
+ describe('clearIndicators', () => {
193
+ it('should clear indicators when they exist', () => {
194
+ const indicators: PointsSnapIndicator[] = [
195
+ {
196
+ id: 'test-indicator',
197
+ type: 'points',
198
+ points: [{ x: 10, y: 20 }],
199
+ },
200
+ ]
201
+
202
+ snapManager.setIndicators(indicators)
203
+ expect(snapManager.getIndicators()).toHaveLength(1)
204
+
205
+ snapManager.clearIndicators()
206
+ expect(snapManager.getIndicators()).toEqual([])
207
+ })
208
+
209
+ it('should not throw when clearing empty indicators', () => {
210
+ expect(() => snapManager.clearIndicators()).not.toThrow()
211
+ expect(snapManager.getIndicators()).toEqual([])
212
+ })
213
+ })
214
+ })
215
+
216
+ describe('getSnapThreshold', () => {
217
+ it('should calculate threshold based on zoom level', () => {
218
+ editor.getZoomLevel.mockReturnValue(1)
219
+ expect(snapManager.getSnapThreshold()).toBe(8)
220
+ })
221
+
222
+ it('should adjust threshold for different zoom levels', () => {
223
+ // Create a new SnapManager for each zoom level to avoid computed value caching
224
+ editor.getZoomLevel.mockReturnValue(2)
225
+ const snapManager2 = new SnapManager(editor)
226
+ expect(snapManager2.getSnapThreshold()).toBe(4)
227
+
228
+ editor.getZoomLevel.mockReturnValue(0.5)
229
+ const snapManager3 = new SnapManager(editor)
230
+ expect(snapManager3.getSnapThreshold()).toBe(16)
231
+ })
232
+
233
+ it('should handle very small zoom levels', () => {
234
+ editor.getZoomLevel.mockReturnValue(0.1)
235
+ expect(snapManager.getSnapThreshold()).toBe(80)
236
+ })
237
+
238
+ it('should handle very large zoom levels', () => {
239
+ editor.getZoomLevel.mockReturnValue(10)
240
+ expect(snapManager.getSnapThreshold()).toBe(0.8)
241
+ })
242
+ })
243
+
244
+ describe('getCurrentCommonAncestor', () => {
245
+ it('should return common ancestor of selected shapes', () => {
246
+ const shapeId = createShapeId('shape1')
247
+ const selectedShapes = [createMockShape(shapeId)]
248
+
249
+ editor.getSelectedShapes.mockReturnValue(selectedShapes as any)
250
+ editor.findCommonAncestor.mockReturnValue(shapeId)
251
+
252
+ expect(snapManager.getCurrentCommonAncestor()).toBe(shapeId)
253
+ expect(editor.findCommonAncestor).toHaveBeenCalledWith(selectedShapes)
254
+ })
255
+
256
+ it('should return null when no common ancestor found', () => {
257
+ editor.getSelectedShapes.mockReturnValue([])
258
+ editor.findCommonAncestor.mockReturnValue(undefined)
259
+
260
+ expect(snapManager.getCurrentCommonAncestor()).toBeUndefined()
261
+ })
262
+ })
263
+
264
+ describe('getSnappableShapes', () => {
265
+ it('should return empty set when no shapes in viewport', () => {
266
+ editor.getSortedChildIdsForParent.mockReturnValue([])
267
+
268
+ const result = snapManager.getSnappableShapes()
269
+ expect(result).toBeInstanceOf(Set)
270
+ expect(result.size).toBe(0)
271
+ })
272
+
273
+ it('should include shapes that can snap and are in viewport', () => {
274
+ const shapeId = createShapeId('shape1')
275
+ const shape = createMockShape(shapeId)
276
+
277
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
278
+ editor.getShape.mockReturnValue(shape as any)
279
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
280
+ editor.getViewportPageBounds.mockReturnValue(new Box(0, 0, 100, 100))
281
+
282
+ const result = snapManager.getSnappableShapes()
283
+ expect(result.has(shapeId)).toBe(true)
284
+ })
285
+
286
+ it('should exclude selected shapes', () => {
287
+ const selectedId = createShapeId('selected')
288
+ const unselectedId = createShapeId('unselected')
289
+
290
+ editor.getSelectedShapeIds.mockReturnValue([selectedId])
291
+ editor.getSortedChildIdsForParent.mockReturnValue([selectedId, unselectedId])
292
+ editor.getShape.mockReturnValue(createMockShape(unselectedId) as any)
293
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
294
+
295
+ const result = snapManager.getSnappableShapes()
296
+ expect(result.has(selectedId)).toBe(false)
297
+ expect(result.has(unselectedId)).toBe(true)
298
+ })
299
+
300
+ it('should exclude shapes that cannot snap', () => {
301
+ const shapeId = createShapeId('shape1')
302
+ const shape = createMockShape(shapeId)
303
+
304
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
305
+ editor.getShape.mockReturnValue(shape as any)
306
+ editor.getShapeUtil.mockReturnValue({
307
+ canSnap: jest.fn(() => false),
308
+ } as any)
309
+
310
+ const result = snapManager.getSnappableShapes()
311
+ expect(result.has(shapeId)).toBe(false)
312
+ })
313
+
314
+ it('should exclude shapes outside viewport bounds', () => {
315
+ const shapeId = createShapeId('shape1')
316
+ const shape = createMockShape(shapeId)
317
+
318
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
319
+ editor.getShape.mockReturnValue(shape as any)
320
+ editor.getShapePageBounds.mockReturnValue(new Box(200, 200, 50, 50))
321
+ editor.getViewportPageBounds.mockReturnValue(new Box(0, 0, 100, 100))
322
+
323
+ const result = snapManager.getSnappableShapes()
324
+ expect(result.has(shapeId)).toBe(false)
325
+ })
326
+
327
+ it('should include frame shapes as snappable', () => {
328
+ const frameId = createShapeId('frame1')
329
+ const frameShape = createMockFrameShape(frameId)
330
+
331
+ editor.getSortedChildIdsForParent.mockReturnValue([frameId])
332
+ editor.getShape.mockReturnValue(frameShape as any)
333
+ editor.isShapeOfType.mockImplementation((_shape, type) => type === 'frame')
334
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
335
+
336
+ const result = snapManager.getSnappableShapes()
337
+ expect(result.has(frameId)).toBe(true)
338
+ })
339
+
340
+ it('should recurse into group shapes but not include group itself', () => {
341
+ const groupId = createShapeId('group1')
342
+ const childId = createShapeId('child1')
343
+ const groupShape = createMockGroupShape(groupId)
344
+ const childShape = createMockShape(childId)
345
+
346
+ editor.getSortedChildIdsForParent
347
+ .mockReturnValueOnce([groupId]) // Root level
348
+ .mockReturnValueOnce([childId]) // Inside group
349
+
350
+ editor.getShape.mockImplementation((id) => {
351
+ if (id === groupId) return groupShape as any
352
+ if (id === childId) return childShape as any
353
+ return undefined
354
+ })
355
+
356
+ editor.isShapeOfType.mockImplementation(
357
+ (shape, type) => shape && (shape as any).type === type
358
+ )
359
+
360
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
361
+
362
+ const result = snapManager.getSnappableShapes()
363
+ expect(result.has(groupId)).toBe(false) // Group itself not included
364
+ expect(result.has(childId)).toBe(true) // Child is included
365
+ })
366
+
367
+ it('should handle nested frame structures', () => {
368
+ const parentFrameId = createShapeId('parent-frame')
369
+ const childFrameId = createShapeId('child-frame')
370
+ const parentFrame = createMockFrameShape(parentFrameId)
371
+ const childFrame = createMockFrameShape(childFrameId)
372
+
373
+ // Override the getCurrentCommonAncestor mock for this specific test
374
+ const originalGetCurrentCommonAncestor = snapManager.getCurrentCommonAncestor
375
+ jest.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(parentFrameId)
376
+
377
+ editor.getSortedChildIdsForParent.mockReturnValueOnce([childFrameId]) // Children of parent frame
378
+
379
+ editor.getShape.mockImplementation((id) => {
380
+ if (id === parentFrameId) return parentFrame as any
381
+ if (id === childFrameId) return childFrame as any
382
+ return undefined
383
+ })
384
+
385
+ editor.isShapeOfType.mockImplementation((shape, type) => type === 'frame')
386
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
387
+
388
+ const result = snapManager.getSnappableShapes()
389
+ expect(result.has(childFrameId)).toBe(true)
390
+
391
+ // Restore original method
392
+ jest
393
+ .spyOn(snapManager, 'getCurrentCommonAncestor')
394
+ .mockImplementation(originalGetCurrentCommonAncestor)
395
+ })
396
+
397
+ it('should handle missing shape bounds gracefully', () => {
398
+ const shapeId = createShapeId('shape1')
399
+ const shape = createMockShape(shapeId)
400
+
401
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
402
+ editor.getShape.mockReturnValue(shape as any)
403
+ editor.getShapePageBounds.mockReturnValue(undefined)
404
+
405
+ const result = snapManager.getSnappableShapes()
406
+ expect(result.has(shapeId)).toBe(false)
407
+ })
408
+
409
+ it('should handle missing shapes gracefully', () => {
410
+ const shapeId = createShapeId('shape1')
411
+
412
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
413
+ editor.getShape.mockReturnValue(undefined)
414
+
415
+ const result = snapManager.getSnappableShapes()
416
+ expect(result.has(shapeId)).toBe(false)
417
+ })
418
+
419
+ it('should use current page as fallback when no common ancestor', () => {
420
+ const shapeId = createShapeId('shape1')
421
+ const shape = createMockShape(shapeId)
422
+
423
+ // Override the getCurrentCommonAncestor mock for this specific test
424
+ const originalGetCurrentCommonAncestor = snapManager.getCurrentCommonAncestor
425
+ jest.spyOn(snapManager, 'getCurrentCommonAncestor').mockReturnValue(undefined)
426
+
427
+ editor.getCurrentPageId.mockReturnValue('page:current' as TLPageId)
428
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
429
+ editor.getShape.mockReturnValue(shape as any)
430
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
431
+
432
+ snapManager.getSnappableShapes()
433
+ expect(editor.getSortedChildIdsForParent).toHaveBeenCalledWith('page:current')
434
+
435
+ // Restore original method
436
+ jest
437
+ .spyOn(snapManager, 'getCurrentCommonAncestor')
438
+ .mockImplementation(originalGetCurrentCommonAncestor)
439
+ })
440
+ })
441
+
442
+ describe('computed properties behavior', () => {
443
+ it('should calculate threshold based on current zoom level', () => {
444
+ // Test with different SnapManager instances to verify the computation works
445
+ editor.getZoomLevel.mockReturnValue(1)
446
+ const snapManager1 = new SnapManager(editor)
447
+ const threshold1 = snapManager1.getSnapThreshold()
448
+
449
+ editor.getZoomLevel.mockReturnValue(2)
450
+ const snapManager2 = new SnapManager(editor)
451
+ const threshold2 = snapManager2.getSnapThreshold()
452
+
453
+ expect(threshold1).toBe(8)
454
+ expect(threshold2).toBe(4)
455
+ })
456
+
457
+ it('should calculate snappable shapes based on current editor state', () => {
458
+ const shapeId1 = createShapeId('shape1')
459
+ const shapeId2 = createShapeId('shape2')
460
+
461
+ // Test with first set of shapes
462
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId1])
463
+ editor.getShape.mockReturnValue(createMockShape(shapeId1) as any)
464
+ editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
465
+
466
+ const snapManager1 = new SnapManager(editor)
467
+ const result1 = snapManager1.getSnappableShapes()
468
+ expect(result1.has(shapeId1)).toBe(true)
469
+ expect(result1.has(shapeId2)).toBe(false)
470
+
471
+ // Test with second set of shapes
472
+ editor.getSortedChildIdsForParent.mockReturnValue([shapeId1, shapeId2])
473
+ editor.getShape.mockImplementation((id) => {
474
+ if (id === shapeId1) return createMockShape(shapeId1) as any
475
+ if (id === shapeId2) return createMockShape(shapeId2) as any
476
+ return undefined
477
+ })
478
+
479
+ const snapManager2 = new SnapManager(editor)
480
+ const result2 = snapManager2.getSnappableShapes()
481
+ expect(result2.has(shapeId1)).toBe(true)
482
+ expect(result2.has(shapeId2)).toBe(true)
483
+ })
484
+ })
485
+ })