@tldraw/editor 3.16.0-next.fe14f1b4181f → 4.0.0

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 (143) hide show
  1. package/dist-cjs/index.d.ts +91 -109
  2. package/dist-cjs/index.js +3 -5
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +1 -7
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +11 -1
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  8. package/dist-cjs/lib/config/TLUserPreferences.js +15 -4
  9. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +58 -114
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  13. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +4 -2
  15. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +11 -6
  17. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  18. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
  19. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  21. package/dist-cjs/lib/hooks/useCanvasEvents.js +19 -16
  22. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  23. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  24. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  26. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  28. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useHandleEvents.js +6 -6
  30. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  32. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  33. package/dist-cjs/lib/hooks/useSelectionEvents.js +8 -8
  34. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  35. package/dist-cjs/lib/license/LicenseManager.js +143 -53
  36. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  37. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  38. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  39. package/dist-cjs/lib/license/Watermark.js +144 -75
  40. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  41. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  42. package/dist-cjs/lib/primitives/Vec.js +0 -4
  43. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  45. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  47. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  48. package/dist-cjs/lib/utils/dom.js.map +2 -2
  49. package/dist-cjs/lib/utils/getPointerInfo.js +2 -3
  50. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  51. package/dist-cjs/lib/utils/reparenting.js +7 -36
  52. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  53. package/dist-cjs/version.js +4 -4
  54. package/dist-cjs/version.js.map +1 -1
  55. package/dist-esm/index.d.mts +91 -109
  56. package/dist-esm/index.mjs +3 -5
  57. package/dist-esm/index.mjs.map +2 -2
  58. package/dist-esm/lib/TldrawEditor.mjs +1 -7
  59. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +11 -1
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  62. package/dist-esm/lib/config/TLUserPreferences.mjs +15 -4
  63. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  64. package/dist-esm/lib/editor/Editor.mjs +58 -114
  65. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  66. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  67. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +4 -2
  69. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +11 -6
  71. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
  73. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/hooks/useCanvasEvents.mjs +20 -22
  75. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  76. package/dist-esm/lib/hooks/useDocumentEvents.mjs +6 -6
  77. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  78. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -2
  79. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  80. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  81. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useHandleEvents.mjs +6 -6
  83. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  85. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/useSelectionEvents.mjs +9 -14
  87. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  88. package/dist-esm/lib/license/LicenseManager.mjs +144 -54
  89. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  90. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  91. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  92. package/dist-esm/lib/license/Watermark.mjs +145 -76
  93. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  94. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  96. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  98. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  99. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  100. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  101. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  102. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -3
  103. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  104. package/dist-esm/lib/utils/reparenting.mjs +8 -41
  105. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  106. package/dist-esm/version.mjs +4 -4
  107. package/dist-esm/version.mjs.map +1 -1
  108. package/editor.css +8 -3
  109. package/package.json +7 -7
  110. package/src/index.ts +2 -9
  111. package/src/lib/TldrawEditor.tsx +1 -15
  112. package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
  113. package/src/lib/config/TLUserPreferences.ts +16 -3
  114. package/src/lib/editor/Editor.test.ts +90 -0
  115. package/src/lib/editor/Editor.ts +77 -151
  116. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  117. package/src/lib/editor/managers/FocusManager/FocusManager.ts +6 -2
  118. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +30 -8
  119. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +10 -3
  120. package/src/lib/editor/shapes/ShapeUtil.ts +32 -0
  121. package/src/lib/editor/types/misc-types.ts +0 -6
  122. package/src/lib/hooks/useCanvasEvents.ts +20 -20
  123. package/src/lib/hooks/useDocumentEvents.ts +6 -6
  124. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  125. package/src/lib/hooks/useGestureEvents.ts +2 -2
  126. package/src/lib/hooks/useHandleEvents.ts +6 -6
  127. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  128. package/src/lib/hooks/useSelectionEvents.ts +9 -14
  129. package/src/lib/license/LicenseManager.test.ts +721 -382
  130. package/src/lib/license/LicenseManager.ts +204 -58
  131. package/src/lib/license/LicenseProvider.tsx +74 -2
  132. package/src/lib/license/Watermark.tsx +152 -77
  133. package/src/lib/license/useLicenseManagerState.ts +2 -2
  134. package/src/lib/primitives/Vec.ts +0 -5
  135. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  136. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  137. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  138. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  139. package/src/lib/utils/dom.test.ts +103 -0
  140. package/src/lib/utils/dom.ts +8 -1
  141. package/src/lib/utils/getPointerInfo.ts +3 -2
  142. package/src/lib/utils/reparenting.ts +10 -70
  143. package/src/version.ts +4 -4
@@ -1,6 +1,7 @@
1
1
  import { Mat } from '../Mat'
2
2
  import { Vec, VecLike } from '../Vec'
3
3
  import { Geometry2dFilters } from './Geometry2d'
4
+ import { Group2d } from './Group2d'
4
5
  import { Rectangle2d } from './Rectangle2d'
5
6
 
6
7
  describe('TransformedGeometry2d', () => {
@@ -36,6 +37,425 @@ describe('TransformedGeometry2d', () => {
36
37
  })
37
38
  })
38
39
 
40
+ describe('excludeFromShapeBounds', () => {
41
+ test('simple geometry with excludeFromShapeBounds flag', () => {
42
+ const rect = new Rectangle2d({
43
+ width: 100,
44
+ height: 50,
45
+ isFilled: true,
46
+ excludeFromShapeBounds: true,
47
+ })
48
+
49
+ // The bounds should still be calculated normally for simple geometry
50
+ const bounds = rect.bounds
51
+ expect(bounds.width).toBe(100)
52
+ expect(bounds.height).toBe(50)
53
+ expect(bounds.x).toBe(0)
54
+ expect(bounds.y).toBe(0)
55
+ })
56
+
57
+ test('group with excluded child geometry', () => {
58
+ const mainRect = new Rectangle2d({
59
+ width: 100,
60
+ height: 50,
61
+ isFilled: true,
62
+ })
63
+
64
+ const excludedRect = new Rectangle2d({
65
+ width: 200,
66
+ height: 100,
67
+ isFilled: true,
68
+ excludeFromShapeBounds: true,
69
+ })
70
+
71
+ const group = new Group2d({
72
+ children: [mainRect, excludedRect],
73
+ })
74
+
75
+ // The bounds should only include the non-excluded rectangle
76
+ const bounds = group.bounds
77
+ expect(bounds.width).toBe(100) // Only the main rectangle width
78
+ expect(bounds.height).toBe(50) // Only the main rectangle height
79
+ expect(bounds.x).toBe(0)
80
+ expect(bounds.y).toBe(0)
81
+ })
82
+
83
+ test('group with multiple excluded children', () => {
84
+ const rect1 = new Rectangle2d({
85
+ width: 50,
86
+ height: 50,
87
+ isFilled: true,
88
+ })
89
+
90
+ const rect2 = new Rectangle2d({
91
+ width: 100,
92
+ height: 30,
93
+ isFilled: true,
94
+ })
95
+
96
+ const excludedRect1 = new Rectangle2d({
97
+ width: 200,
98
+ height: 200,
99
+ isFilled: true,
100
+ excludeFromShapeBounds: true,
101
+ })
102
+
103
+ const excludedRect2 = new Rectangle2d({
104
+ width: 300,
105
+ height: 300,
106
+ isFilled: true,
107
+ excludeFromShapeBounds: true,
108
+ })
109
+
110
+ const group = new Group2d({
111
+ children: [rect1, excludedRect1, rect2, excludedRect2],
112
+ })
113
+
114
+ // The bounds should include both non-excluded rectangles
115
+ const bounds = group.bounds
116
+ expect(bounds.width).toBe(100) // Width of rect2 (larger of the two)
117
+ expect(bounds.height).toBe(50) // Height of rect1 (larger of the two)
118
+ expect(bounds.x).toBe(0)
119
+ expect(bounds.y).toBe(0)
120
+ })
121
+
122
+ test('group with all children excluded', () => {
123
+ const excludedRect1 = new Rectangle2d({
124
+ width: 100,
125
+ height: 50,
126
+ isFilled: true,
127
+ excludeFromShapeBounds: true,
128
+ })
129
+
130
+ const excludedRect2 = new Rectangle2d({
131
+ width: 200,
132
+ height: 100,
133
+ isFilled: true,
134
+ excludeFromShapeBounds: true,
135
+ })
136
+
137
+ const group = new Group2d({
138
+ children: [excludedRect1, excludedRect2],
139
+ })
140
+
141
+ // The bounds should be empty when all children are excluded
142
+ const bounds = group.bounds
143
+ expect(bounds.width).toBe(0)
144
+ expect(bounds.height).toBe(0)
145
+ expect(bounds.x).toBe(0)
146
+ expect(bounds.y).toBe(0)
147
+ })
148
+
149
+ test('nested groups with excluded geometry', () => {
150
+ const innerRect = new Rectangle2d({
151
+ width: 50,
152
+ height: 50,
153
+ isFilled: true,
154
+ })
155
+
156
+ const excludedRect = new Rectangle2d({
157
+ width: 200,
158
+ height: 200,
159
+ isFilled: true,
160
+ excludeFromShapeBounds: true,
161
+ })
162
+
163
+ const innerGroup = new Group2d({
164
+ children: [innerRect, excludedRect],
165
+ })
166
+
167
+ const outerRect = new Rectangle2d({
168
+ width: 100,
169
+ height: 30,
170
+ isFilled: true,
171
+ })
172
+
173
+ const outerGroup = new Group2d({
174
+ children: [innerGroup, outerRect],
175
+ })
176
+
177
+ // The bounds should include both the inner group (without excluded rect) and outer rect
178
+ const bounds = outerGroup.bounds
179
+ expect(bounds.width).toBe(100) // Width of outerRect (larger)
180
+ expect(bounds.height).toBe(50) // Height of innerRect (larger)
181
+ expect(bounds.x).toBe(0)
182
+ expect(bounds.y).toBe(0)
183
+ })
184
+
185
+ test('bounds calculation with transformed geometry', () => {
186
+ const rect = new Rectangle2d({
187
+ width: 50,
188
+ height: 50,
189
+ isFilled: true,
190
+ }).transform(Mat.Translate(100, 100))
191
+
192
+ const excludedRect = new Rectangle2d({
193
+ width: 200,
194
+ height: 200,
195
+ isFilled: true,
196
+ excludeFromShapeBounds: true,
197
+ }).transform(Mat.Translate(50, 50))
198
+
199
+ const group = new Group2d({
200
+ children: [rect, excludedRect],
201
+ })
202
+
203
+ // The bounds should only include the non-excluded rectangle
204
+ const bounds = group.bounds
205
+ // Verify that the excluded rectangle doesn't affect the bounds
206
+ // The bounds should be smaller than if the excluded rect was included
207
+ expect(bounds.width).toBeLessThan(200) // Should not include the excluded rect's width
208
+ expect(bounds.height).toBeLessThan(200) // Should not include the excluded rect's height
209
+ // The bounds should not be empty
210
+ expect(bounds.width).toBeGreaterThan(0)
211
+ expect(bounds.height).toBeGreaterThan(0)
212
+ })
213
+ })
214
+
215
+ describe('getBoundsVertices', () => {
216
+ test('basic geometry returns vertices when not excluded from bounds', () => {
217
+ const rect = new Rectangle2d({
218
+ width: 100,
219
+ height: 50,
220
+ isFilled: true,
221
+ })
222
+
223
+ const boundsVertices = rect.getBoundsVertices()
224
+ const vertices = rect.getVertices()
225
+
226
+ expect(boundsVertices).toEqual(vertices)
227
+ expect(boundsVertices.length).toBe(4)
228
+ expect(boundsVertices).toMatchObject([
229
+ { x: 0, y: 0, z: 1 },
230
+ { x: 100, y: 0, z: 1 },
231
+ { x: 100, y: 50, z: 1 },
232
+ { x: 0, y: 50, z: 1 },
233
+ ])
234
+ })
235
+
236
+ test('geometry excluded from shape bounds returns empty array', () => {
237
+ const rect = new Rectangle2d({
238
+ width: 100,
239
+ height: 50,
240
+ isFilled: true,
241
+ excludeFromShapeBounds: true,
242
+ })
243
+
244
+ const boundsVertices = rect.getBoundsVertices()
245
+ expect(boundsVertices).toEqual([])
246
+ })
247
+
248
+ test('cached boundsVertices property', () => {
249
+ const rect = new Rectangle2d({
250
+ width: 100,
251
+ height: 50,
252
+ isFilled: true,
253
+ })
254
+
255
+ // Access the cached property multiple times
256
+ const boundsVertices1 = rect.boundsVertices
257
+ const boundsVertices2 = rect.boundsVertices
258
+
259
+ // Should return the same reference (cached)
260
+ expect(boundsVertices1).toBe(boundsVertices2)
261
+ expect(boundsVertices1.length).toBe(4)
262
+ })
263
+ })
264
+
265
+ describe('TransformedGeometry2d getBoundsVertices', () => {
266
+ test('transforms bounds vertices correctly', () => {
267
+ const rect = new Rectangle2d({
268
+ width: 100,
269
+ height: 50,
270
+ isFilled: true,
271
+ })
272
+
273
+ const transformed = rect.transform(Mat.Translate(50, 100).scale(2, 2))
274
+ const boundsVertices = transformed.getBoundsVertices()
275
+
276
+ expect(boundsVertices).toMatchObject([
277
+ { x: 50, y: 100, z: 1 },
278
+ { x: 250, y: 100, z: 1 },
279
+ { x: 250, y: 200, z: 1 },
280
+ { x: 50, y: 200, z: 1 },
281
+ ])
282
+ })
283
+
284
+ test('transforms empty bounds vertices for excluded geometry', () => {
285
+ const rect = new Rectangle2d({
286
+ width: 100,
287
+ height: 50,
288
+ isFilled: true,
289
+ excludeFromShapeBounds: true,
290
+ })
291
+
292
+ const transformed = rect.transform(Mat.Translate(50, 100))
293
+ const boundsVertices = transformed.getBoundsVertices()
294
+
295
+ expect(boundsVertices).toEqual([])
296
+ })
297
+
298
+ test('nested transform preserves bounds vertices behavior', () => {
299
+ const rect = new Rectangle2d({
300
+ width: 100,
301
+ height: 50,
302
+ isFilled: true,
303
+ })
304
+
305
+ const transformed1 = rect.transform(Mat.Translate(10, 20))
306
+ const transformed2 = transformed1.transform(Mat.Scale(2, 2))
307
+ const boundsVertices = transformed2.getBoundsVertices()
308
+
309
+ expect(boundsVertices).toMatchObject([
310
+ { x: 20, y: 40, z: 1 },
311
+ { x: 220, y: 40, z: 1 },
312
+ { x: 220, y: 140, z: 1 },
313
+ { x: 20, y: 140, z: 1 },
314
+ ])
315
+ })
316
+ })
317
+
318
+ describe('Group2d getBoundsVertices', () => {
319
+ test('flattens children bounds vertices', () => {
320
+ const rect1 = new Rectangle2d({
321
+ width: 50,
322
+ height: 50,
323
+ isFilled: true,
324
+ })
325
+
326
+ const rect2 = new Rectangle2d({
327
+ width: 30,
328
+ height: 30,
329
+ isFilled: true,
330
+ }).transform(Mat.Translate(60, 60))
331
+
332
+ const group = new Group2d({
333
+ children: [rect1, rect2],
334
+ })
335
+
336
+ const boundsVertices = group.getBoundsVertices()
337
+
338
+ // Should include all vertices from both rectangles
339
+ expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
340
+
341
+ // Check that we have vertices from both rectangles
342
+ expect(boundsVertices).toEqual(
343
+ expect.arrayContaining([
344
+ expect.objectContaining({ x: 0, y: 0 }), // rect1 vertices
345
+ expect.objectContaining({ x: 50, y: 0 }),
346
+ expect.objectContaining({ x: 50, y: 50 }),
347
+ expect.objectContaining({ x: 0, y: 50 }),
348
+ expect.objectContaining({ x: 60, y: 60 }), // rect2 vertices
349
+ expect.objectContaining({ x: 90, y: 60 }),
350
+ expect.objectContaining({ x: 90, y: 90 }),
351
+ expect.objectContaining({ x: 60, y: 90 }),
352
+ ])
353
+ )
354
+ })
355
+
356
+ test('excludes children marked as excluded from bounds', () => {
357
+ const rect1 = new Rectangle2d({
358
+ width: 50,
359
+ height: 50,
360
+ isFilled: true,
361
+ })
362
+
363
+ const rect2 = new Rectangle2d({
364
+ width: 100,
365
+ height: 100,
366
+ isFilled: true,
367
+ excludeFromShapeBounds: true,
368
+ })
369
+
370
+ const group = new Group2d({
371
+ children: [rect1, rect2],
372
+ })
373
+
374
+ const boundsVertices = group.getBoundsVertices()
375
+
376
+ // Should only include vertices from rect1, not rect2
377
+ expect(boundsVertices.length).toBe(4) // Only rect1's 4 vertices
378
+ expect(boundsVertices).toMatchObject([
379
+ { x: 0, y: 0, z: 1 },
380
+ { x: 50, y: 0, z: 1 },
381
+ { x: 50, y: 50, z: 1 },
382
+ { x: 0, y: 50, z: 1 },
383
+ ])
384
+ })
385
+
386
+ test('returns empty array when group itself is excluded from bounds', () => {
387
+ const rect1 = new Rectangle2d({
388
+ width: 50,
389
+ height: 50,
390
+ isFilled: true,
391
+ })
392
+
393
+ const rect2 = new Rectangle2d({
394
+ width: 30,
395
+ height: 30,
396
+ isFilled: true,
397
+ })
398
+
399
+ const group = new Group2d({
400
+ children: [rect1, rect2],
401
+ excludeFromShapeBounds: true,
402
+ })
403
+
404
+ const boundsVertices = group.getBoundsVertices()
405
+ expect(boundsVertices).toEqual([])
406
+ })
407
+
408
+ test('handles nested groups correctly', () => {
409
+ const rect1 = new Rectangle2d({
410
+ width: 50,
411
+ height: 50,
412
+ isFilled: true,
413
+ })
414
+
415
+ const rect2 = new Rectangle2d({
416
+ width: 30,
417
+ height: 30,
418
+ isFilled: true,
419
+ })
420
+
421
+ const innerGroup = new Group2d({
422
+ children: [rect2],
423
+ })
424
+
425
+ const outerGroup = new Group2d({
426
+ children: [rect1, innerGroup],
427
+ })
428
+
429
+ const boundsVertices = outerGroup.getBoundsVertices()
430
+
431
+ // Should include vertices from both rectangles
432
+ expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
433
+ })
434
+
435
+ test('handles all children excluded from bounds', () => {
436
+ const rect1 = new Rectangle2d({
437
+ width: 50,
438
+ height: 50,
439
+ isFilled: true,
440
+ excludeFromShapeBounds: true,
441
+ })
442
+
443
+ const rect2 = new Rectangle2d({
444
+ width: 30,
445
+ height: 30,
446
+ isFilled: true,
447
+ excludeFromShapeBounds: true,
448
+ })
449
+
450
+ const group = new Group2d({
451
+ children: [rect1, rect2],
452
+ })
453
+
454
+ const boundsVertices = group.getBoundsVertices()
455
+ expect(boundsVertices).toEqual([])
456
+ })
457
+ })
458
+
39
459
  function expectApproxMatch(a: VecLike, b: VecLike) {
40
460
  expect(a.x).toBeCloseTo(b.x, 0.0001)
41
461
  expect(a.y).toBeCloseTo(b.y, 0.0001)
@@ -9,6 +9,8 @@ import {
9
9
  intersectLineSegmentPolyline,
10
10
  intersectPolys,
11
11
  linesIntersect,
12
+ polygonIntersectsPolyline,
13
+ polygonsIntersect,
12
14
  } from '../intersect'
13
15
  import { approximately, pointInPolygon } from '../utils'
14
16
 
@@ -48,6 +50,7 @@ export interface TransformedGeometry2dOptions {
48
50
  isInternal?: boolean
49
51
  debugColor?: string
50
52
  ignore?: boolean
53
+ excludeFromShapeBounds?: boolean
51
54
  }
52
55
 
53
56
  /** @public */
@@ -64,11 +67,17 @@ export abstract class Geometry2d {
64
67
  isLabel = false
65
68
  isEmptyLabel = false
66
69
  isInternal = false
70
+ excludeFromShapeBounds = false
67
71
  debugColor?: string
68
72
  ignore?: boolean
69
73
 
70
74
  constructor(opts: Geometry2dOptions) {
71
- const { isLabel = false, isEmptyLabel = false, isInternal = false } = opts
75
+ const {
76
+ isLabel = false,
77
+ isEmptyLabel = false,
78
+ isInternal = false,
79
+ excludeFromShapeBounds = false,
80
+ } = opts
72
81
  this.isFilled = opts.isFilled
73
82
  this.isClosed = opts.isClosed
74
83
  this.debugColor = opts.debugColor
@@ -76,6 +85,7 @@ export abstract class Geometry2d {
76
85
  this.isLabel = isLabel
77
86
  this.isEmptyLabel = isEmptyLabel
78
87
  this.isInternal = isInternal
88
+ this.excludeFromShapeBounds = excludeFromShapeBounds
79
89
  }
80
90
 
81
91
  isExcludedByFilter(filters?: Geometry2dFilters) {
@@ -227,25 +237,6 @@ export abstract class Geometry2d {
227
237
  return distanceAlongRoute / length
228
238
  }
229
239
 
230
- /** @deprecated Iterate the vertices instead. */
231
- nearestPointOnLineSegment(A: VecLike, B: VecLike): Vec {
232
- const { vertices } = this
233
- let nearest: Vec | undefined
234
- let dist = Infinity
235
- let d: number, p: Vec, q: Vec
236
- for (let i = 0; i < vertices.length; i++) {
237
- p = vertices[i]
238
- q = Vec.NearestPointOnLineSegment(A, B, p, true)
239
- d = Vec.Dist2(p, q)
240
- if (d < dist) {
241
- dist = d
242
- nearest = q
243
- }
244
- }
245
- if (!nearest) throw Error('nearest point not found')
246
- return nearest
247
- }
248
-
249
240
  isPointInBounds(point: VecLike, margin = 0) {
250
241
  const { bounds } = this
251
242
  return !(
@@ -256,6 +247,53 @@ export abstract class Geometry2d {
256
247
  )
257
248
  }
258
249
 
250
+ overlapsPolygon(_polygon: VecLike[]): boolean {
251
+ const polygon = _polygon.map((v) => Vec.From(v))
252
+
253
+ // Otherwise, check if the geometry itself overlaps the polygon
254
+ const { vertices, center, isFilled, isEmptyLabel, isClosed } = this
255
+
256
+ // We'll do things in order of cheapest to most expensive checks
257
+
258
+ // Skip empty labels
259
+ if (isEmptyLabel) return false
260
+
261
+ // If any of the geometry's vertices are inside the polygon, it's inside
262
+ if (vertices.some((v) => pointInPolygon(v, polygon))) {
263
+ return true
264
+ }
265
+
266
+ // If the geometry is filled and closed and its center is inside the polygon, it's inside
267
+ if (isClosed) {
268
+ if (isFilled) {
269
+ // If closed and filled, check if the center is inside the polygon
270
+ if (pointInPolygon(center, polygon)) {
271
+ return true
272
+ }
273
+
274
+ // ..then, slightly more expensive check, see the geometry covers the entire polygon but not its center
275
+ if (polygon.every((v) => pointInPolygon(v, vertices))) {
276
+ return true
277
+ }
278
+ }
279
+
280
+ // If any the geometry's vertices intersect the edge of the polygon, it's inside.
281
+ // for example when a rotated rectangle is moved over the corner of a parent rectangle
282
+ // If the geometry is closed, intersect as a polygon
283
+ if (polygonsIntersect(polygon, vertices)) {
284
+ return true
285
+ }
286
+ } else {
287
+ // If the geometry is not closed, intersect as a polyline
288
+ if (polygonIntersectsPolyline(polygon, vertices)) {
289
+ return true
290
+ }
291
+ }
292
+
293
+ // If none of the above checks passed, the geometry is outside the polygon
294
+ return false
295
+ }
296
+
259
297
  transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
260
298
  return new TransformedGeometry2d(this, transform, opts)
261
299
  }
@@ -271,8 +309,23 @@ export abstract class Geometry2d {
271
309
  return this._vertices
272
310
  }
273
311
 
312
+ getBoundsVertices(): Vec[] {
313
+ if (this.excludeFromShapeBounds) return []
314
+ return this.vertices
315
+ }
316
+
317
+ private _boundsVertices: Vec[] | undefined
318
+
319
+ // eslint-disable-next-line no-restricted-syntax
320
+ get boundsVertices(): Vec[] {
321
+ if (!this._boundsVertices) {
322
+ this._boundsVertices = this.getBoundsVertices()
323
+ }
324
+ return this._boundsVertices
325
+ }
326
+
274
327
  getBounds() {
275
- return Box.FromPoints(this.vertices)
328
+ return Box.FromPoints(this.boundsVertices)
276
329
  }
277
330
 
278
331
  private _bounds: Box | undefined
@@ -399,6 +452,10 @@ export class TransformedGeometry2d extends Geometry2d {
399
452
  return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
400
453
  }
401
454
 
455
+ getBoundsVertices(): Vec[] {
456
+ return this.geometry.getBoundsVertices().map((v) => Mat.applyToPoint(this.matrix, v))
457
+ }
458
+
402
459
  nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
403
460
  return Mat.applyToPoint(
404
461
  this.matrix,
@@ -114,6 +114,11 @@ export class Group2d extends Geometry2d {
114
114
  })
115
115
  }
116
116
 
117
+ override getBoundsVertices(): Vec[] {
118
+ if (this.excludeFromShapeBounds) return []
119
+ return this.children.flatMap((child) => child.getBoundsVertices())
120
+ }
121
+
117
122
  override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
118
123
  return this.children.flatMap((child) => {
119
124
  if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
@@ -205,7 +210,7 @@ export class Group2d extends Geometry2d {
205
210
  path += child.toSimpleSvgPath()
206
211
  }
207
212
 
208
- const corners = Box.FromPoints(this.vertices).corners
213
+ const corners = Box.FromPoints(this.boundsVertices).corners
209
214
  // draw just a few pixels around each corner, e.g. an L shape for the bottom left
210
215
 
211
216
  for (let i = 0, n = corners.length; i < n; i++) {
@@ -236,4 +241,8 @@ export class Group2d extends Geometry2d {
236
241
  getSvgPathData(): string {
237
242
  return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
238
243
  }
244
+
245
+ overlapsPolygon(polygon: VecLike[]): boolean {
246
+ return this.children.some((child) => child.overlapsPolygon(polygon))
247
+ }
239
248
  }