@tldraw/editor 5.4.0-next.09eca5dee30d → 5.4.0-next.0a83c4ec44f1

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 (249) hide show
  1. package/dist-cjs/index.d.ts +114 -47
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
  9. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
  12. package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/config/createTLStore.js +1 -1
  15. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  16. package/dist-cjs/lib/editor/Editor.js +181 -294
  17. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  18. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  19. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  21. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  23. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  25. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  27. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  29. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  31. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  33. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  35. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  37. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  39. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  41. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  42. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  43. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  44. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  45. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  46. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  47. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  48. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  49. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  50. package/dist-cjs/lib/globals/editors.js +57 -0
  51. package/dist-cjs/lib/globals/editors.js.map +7 -0
  52. package/dist-cjs/lib/globals/menus.js +9 -4
  53. package/dist-cjs/lib/globals/menus.js.map +2 -2
  54. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  55. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  56. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  57. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  58. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  59. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  60. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  61. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  63. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  64. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  65. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  66. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  67. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  68. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  69. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  70. package/dist-cjs/lib/primitives/Box.js +2 -1
  71. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  72. package/dist-cjs/lib/primitives/Vec.js +4 -3
  73. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  74. package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
  75. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  76. package/dist-cjs/lib/primitives/utils.js +2 -1
  77. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  78. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  79. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  80. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  81. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  82. package/dist-cjs/lib/utils/rotation.js +1 -9
  83. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  84. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  85. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  86. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  87. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  88. package/dist-cjs/version.js +3 -3
  89. package/dist-cjs/version.js.map +1 -1
  90. package/dist-esm/index.d.mts +114 -47
  91. package/dist-esm/index.mjs +9 -2
  92. package/dist-esm/index.mjs.map +2 -2
  93. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  94. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  95. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  96. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  97. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  98. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  99. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  100. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  101. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  102. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  103. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  104. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  105. package/dist-esm/lib/editor/Editor.mjs +189 -295
  106. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  107. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  108. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  109. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  110. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  111. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  112. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  113. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  114. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  115. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  116. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  117. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  118. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  119. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  120. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  121. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  122. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  123. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  124. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  126. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  128. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  130. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  131. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  132. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  133. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  134. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  135. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  136. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  137. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  138. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  139. package/dist-esm/lib/globals/editors.mjs +37 -0
  140. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  141. package/dist-esm/lib/globals/menus.mjs +9 -4
  142. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  143. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  144. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  145. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  146. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  147. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  148. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  149. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  150. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  151. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  152. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  153. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  154. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  155. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  156. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  157. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  158. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  159. package/dist-esm/lib/primitives/Box.mjs +2 -1
  160. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  161. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  162. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  163. package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
  164. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  165. package/dist-esm/lib/primitives/utils.mjs +2 -1
  166. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  167. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  168. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  169. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  170. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  171. package/dist-esm/lib/utils/rotation.mjs +1 -9
  172. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  173. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  174. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  175. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  176. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  177. package/dist-esm/version.mjs +3 -3
  178. package/dist-esm/version.mjs.map +1 -1
  179. package/package.json +13 -7
  180. package/src/index.ts +6 -1
  181. package/src/lib/TldrawEditor.tsx +16 -4
  182. package/src/lib/components/MenuClickCapture.tsx +19 -18
  183. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  184. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  185. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  186. package/src/lib/config/TLUserPreferences.ts +8 -4
  187. package/src/lib/config/createTLStore.ts +1 -1
  188. package/src/lib/editor/Editor.ts +266 -383
  189. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  190. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  191. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  192. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  193. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  194. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  195. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  196. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  197. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  198. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  199. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  200. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  201. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  202. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  203. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  204. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  205. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  206. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  207. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  208. package/src/lib/editor/tools/StateNode.ts +12 -2
  209. package/src/lib/globals/editors.test.ts +76 -0
  210. package/src/lib/globals/editors.ts +53 -0
  211. package/src/lib/globals/menus.test.ts +33 -0
  212. package/src/lib/globals/menus.ts +11 -4
  213. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  214. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  215. package/src/lib/hooks/useLocalStore.ts +16 -8
  216. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  217. package/src/lib/hooks/usePeerIds.ts +2 -1
  218. package/src/lib/license/LicenseManager.test.ts +160 -0
  219. package/src/lib/license/LicenseManager.ts +13 -4
  220. package/src/lib/license/LicenseProvider.tsx +24 -3
  221. package/src/lib/license/useLicenseManagerState.ts +8 -6
  222. package/src/lib/primitives/Box.test.ts +9 -1
  223. package/src/lib/primitives/Box.ts +3 -1
  224. package/src/lib/primitives/Vec.test.ts +10 -0
  225. package/src/lib/primitives/Vec.ts +4 -4
  226. package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
  227. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
  228. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  229. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  230. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  231. package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
  232. package/src/lib/primitives/geometry/Group2d.ts +63 -27
  233. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  234. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  235. package/src/lib/primitives/utils.test.ts +10 -1
  236. package/src/lib/primitives/utils.ts +4 -1
  237. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  238. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  239. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  240. package/src/lib/utils/deepLinks.test.ts +15 -0
  241. package/src/lib/utils/deepLinks.ts +7 -1
  242. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  243. package/src/lib/utils/normalizeWheel.ts +6 -0
  244. package/src/lib/utils/rotation.ts +4 -12
  245. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  246. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  247. package/src/lib/utils/sync/alerts.ts +1 -1
  248. package/src/version.ts +3 -3
  249. package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
@@ -1,5 +1,400 @@
1
- describe('Group2d', () => {
2
- it('should be tested', () => {
3
- expect(true).toBe(true)
1
+ import { Mat } from '../Mat'
2
+ import { Vec, VecLike } from '../Vec'
3
+ import { Edge2d } from './Edge2d'
4
+ import { Geometry2dFilters, TransformedGeometry2d } from './Geometry2d'
5
+ import { Group2d } from './Group2d'
6
+ import { Rectangle2d } from './Rectangle2d'
7
+
8
+ // Two rectangles side by side with a gap between them
9
+ const rectA = () => new Rectangle2d({ width: 100, height: 50, isFilled: true })
10
+ const rectB = () => new Rectangle2d({ x: 200, width: 50, height: 50, isFilled: false })
11
+ // A small filled label to the right of both, and an internal geometry further right
12
+ const label = () =>
13
+ new Rectangle2d({ x: 300, width: 20, height: 20, isFilled: true, isLabel: true })
14
+ const internal = () =>
15
+ new Rectangle2d({ x: 400, width: 10, height: 10, isFilled: true, isInternal: true })
16
+
17
+ const group = () => new Group2d({ children: [rectA(), rectB()] })
18
+ const groupWithLabel = () => new Group2d({ children: [rectA(), label(), rectB()] })
19
+
20
+ // An L shaped path made of two edges, 200 units long in total
21
+ const path = () =>
22
+ new Group2d({
23
+ children: [
24
+ new Edge2d({ start: new Vec(0, 0), end: new Vec(100, 0) }),
25
+ new Edge2d({ start: new Vec(100, 0), end: new Vec(100, 100) }),
26
+ ],
27
+ })
28
+
29
+ function sortByX(points: VecLike[]) {
30
+ const round = (n: number) => Math.round(n * 1e6) / 1e6
31
+ return [...points]
32
+ .map((p) => ({ x: round(p.x), y: round(p.y) }))
33
+ .sort((a, b) => a.x - b.x || a.y - b.y)
34
+ }
35
+
36
+ describe('Group2d construction', () => {
37
+ it('is closed and unfilled and passes through the other options', () => {
38
+ const g = new Group2d({
39
+ children: [rectA()],
40
+ isLabel: true,
41
+ debugColor: 'red',
42
+ excludeFromShapeBounds: true,
43
+ })
44
+ expect(g).toMatchObject({
45
+ isClosed: true,
46
+ isFilled: false,
47
+ isLabel: true,
48
+ debugColor: 'red',
49
+ excludeFromShapeBounds: true,
50
+ })
51
+ })
52
+
53
+ it('keeps the children in order', () => {
54
+ const a = rectA()
55
+ const b = rectB()
56
+ const g = new Group2d({ children: [a, b] })
57
+ expect(g.children).toEqual([a, b])
58
+ expect(g.ignoredChildren).toEqual([])
59
+ })
60
+
61
+ it('flattens nested groups', () => {
62
+ const a = rectA()
63
+ const b = rectB()
64
+ const c = label()
65
+ const g = new Group2d({ children: [a, new Group2d({ children: [b, c] })] })
66
+ expect(g.children).toEqual([a, b, c])
67
+ })
68
+
69
+ it('separates ignored children', () => {
70
+ const a = rectA()
71
+ const ignored = new Rectangle2d({ width: 10, height: 10, isFilled: true, ignore: true })
72
+ const g = new Group2d({ children: [a, ignored] })
73
+ expect(g.children).toEqual([a])
74
+ expect(g.ignoredChildren).toEqual([ignored])
75
+ })
76
+
77
+ it('throws without any non-ignored children', () => {
78
+ expect(() => new Group2d({ children: [] })).toThrow('Group2d must have at least one child')
79
+ expect(
80
+ () =>
81
+ new Group2d({
82
+ children: [new Rectangle2d({ width: 10, height: 10, isFilled: true, ignore: true })],
83
+ })
84
+ ).toThrow('Group2d must have at least one child')
85
+ })
86
+ })
87
+
88
+ describe('Group2d.getVertices', () => {
89
+ it('concatenates the vertices of the children', () => {
90
+ expect(group().vertices).toMatchObject([
91
+ { x: 0, y: 0 },
92
+ { x: 100, y: 0 },
93
+ { x: 100, y: 50 },
94
+ { x: 0, y: 50 },
95
+ { x: 200, y: 0 },
96
+ { x: 250, y: 0 },
97
+ { x: 250, y: 50 },
98
+ { x: 200, y: 50 },
99
+ ])
100
+ })
101
+
102
+ it('excludes labels by default but keeps internal geometry', () => {
103
+ const g = new Group2d({ children: [rectA(), label(), internal()] })
104
+ expect(g.vertices.length).toBe(8)
105
+ expect(g.getVertices(Geometry2dFilters.EXCLUDE_LABELS).length).toBe(8)
106
+ expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(12)
107
+ expect(g.getVertices(Geometry2dFilters.EXCLUDE_INTERNAL).length).toBe(8)
108
+ expect(g.getVertices(Geometry2dFilters.EXCLUDE_NON_STANDARD).length).toBe(4)
109
+ })
110
+
111
+ it('returns nothing when the group itself is excluded by the filter', () => {
112
+ const g = new Group2d({ children: [rectA()], isLabel: true })
113
+ expect(g.getVertices(Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
114
+ expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(4)
115
+ })
116
+
117
+ it('ignores ignored children', () => {
118
+ const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
119
+ const g = new Group2d({ children: [rectA(), ignored] })
120
+ expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(4)
121
+ })
122
+ })
123
+
124
+ describe('Group2d.bounds', () => {
125
+ it('is the union of the children bounds', () => {
126
+ expect(group().bounds).toMatchObject({ x: 0, y: 0, w: 250, h: 50 })
127
+ expect(group().center).toMatchObject({ x: 125, y: 25 })
128
+ })
129
+
130
+ it('includes labels', () => {
131
+ expect(groupWithLabel().bounds).toMatchObject({ x: 0, y: 0, w: 320, h: 50 })
132
+ })
133
+ })
134
+
135
+ describe('Group2d.getLength', () => {
136
+ it('sums the child lengths, excluding labels by default', () => {
137
+ expect(group().length).toBe(500)
138
+ expect(groupWithLabel().length).toBe(500)
139
+ expect(groupWithLabel().getLength(Geometry2dFilters.INCLUDE_ALL)).toBe(580)
140
+ expect(groupWithLabel().getLength(Geometry2dFilters.EXCLUDE_LABELS)).toBe(500)
141
+ })
142
+ })
143
+
144
+ describe('Group2d.getArea', () => {
145
+ it('uses the area of the first child', () => {
146
+ expect(group().area).toBe(5000)
147
+ expect(new Group2d({ children: [rectB(), rectA()] }).area).toBe(2500)
148
+ })
149
+ })
150
+
151
+ describe('Group2d.nearestPoint', () => {
152
+ it('returns the nearest point across the children, preferring earlier children on ties', () => {
153
+ expect(group().nearestPoint(new Vec(190, 25))).toMatchObject({ x: 200, y: 25 })
154
+ expect(group().nearestPoint(new Vec(50, -10))).toMatchObject({ x: 50, y: 0 })
155
+ expect(group().nearestPoint(new Vec(150, 25))).toMatchObject({ x: 100, y: 25 })
156
+ })
157
+
158
+ it('respects the filters', () => {
159
+ const g = groupWithLabel()
160
+ expect(g.nearestPoint(new Vec(310, 30), Geometry2dFilters.INCLUDE_ALL)).toMatchObject({
161
+ x: 310,
162
+ y: 20,
163
+ })
164
+ expect(g.nearestPoint(new Vec(310, 30), Geometry2dFilters.EXCLUDE_LABELS)).toMatchObject({
165
+ x: 250,
166
+ y: 30,
167
+ })
168
+ })
169
+
170
+ it('uses the label when no filters are given', () => {
171
+ expect(groupWithLabel().nearestPoint(new Vec(310, 30))).toMatchObject({ x: 310, y: 20 })
172
+ })
173
+ })
174
+
175
+ describe('Group2d.distanceToPoint', () => {
176
+ it('is the smallest child distance, negative inside a filled child', () => {
177
+ expect(group().distanceToPoint(new Vec(50, 25))).toBe(-25)
178
+ expect(group().distanceToPoint(new Vec(225, 25))).toBe(25)
179
+ expect(group().distanceToPoint(new Vec(225, 25), true)).toBe(-25)
180
+ expect(group().distanceToPoint(new Vec(150, 25))).toBe(50)
181
+ })
182
+
183
+ it('respects the filters', () => {
184
+ const g = groupWithLabel()
185
+ expect(g.distanceToPoint(new Vec(310, 10), false, Geometry2dFilters.INCLUDE_ALL)).toBe(-10)
186
+ expect(g.distanceToPoint(new Vec(310, 10), false, Geometry2dFilters.EXCLUDE_LABELS)).toBe(60)
187
+ })
188
+ })
189
+
190
+ describe('Group2d.hitTestPoint', () => {
191
+ it('hits when any child hits', () => {
192
+ expect(group().hitTestPoint(new Vec(50, 25), 0, false)).toBe(true)
193
+ expect(group().hitTestPoint(new Vec(225, 25), 0, false)).toBe(false)
194
+ expect(group().hitTestPoint(new Vec(225, 25), 0, true)).toBe(true)
195
+ expect(group().hitTestPoint(new Vec(150, 25), 0, true)).toBe(false)
196
+ expect(group().hitTestPoint(new Vec(150, 25), 50, false)).toBe(true)
197
+ })
198
+
199
+ it('excludes labels by default', () => {
200
+ const g = groupWithLabel()
201
+ expect(g.hitTestPoint(new Vec(310, 10), 0, false)).toBe(false)
202
+ expect(g.hitTestPoint(new Vec(310, 10), 0, false, Geometry2dFilters.INCLUDE_ALL)).toBe(true)
203
+ })
204
+
205
+ it('never hits ignored children', () => {
206
+ const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
207
+ const g = new Group2d({ children: [rectA(), ignored] })
208
+ expect(g.hitTestPoint(new Vec(505, 5), 0, true)).toBe(false)
209
+ })
210
+ })
211
+
212
+ describe('Group2d.hitTestLineSegment', () => {
213
+ it('hits when any child hits', () => {
214
+ expect(group().hitTestLineSegment(new Vec(90, 25), new Vec(110, 25), 0)).toBe(true)
215
+ expect(group().hitTestLineSegment(new Vec(240, 25), new Vec(260, 25), 0)).toBe(true)
216
+ expect(group().hitTestLineSegment(new Vec(150, -10), new Vec(150, 60), 0)).toBe(false)
217
+ })
218
+
219
+ it('passes the distance to the children', () => {
220
+ expect(group().hitTestLineSegment(new Vec(150, 0), new Vec(150, 50), 50)).toBe(true)
221
+ expect(group().hitTestLineSegment(new Vec(150, 0), new Vec(150, 50), 49)).toBe(false)
222
+ })
223
+
224
+ it('excludes labels by default', () => {
225
+ const g = groupWithLabel()
226
+ expect(g.hitTestLineSegment(new Vec(310, -10), new Vec(310, 30), 0)).toBe(false)
227
+ expect(
228
+ g.hitTestLineSegment(new Vec(310, -10), new Vec(310, 30), 0, Geometry2dFilters.INCLUDE_ALL)
229
+ ).toBe(true)
230
+ })
231
+ })
232
+
233
+ describe('Group2d.intersectLineSegment', () => {
234
+ it('collects the intersections from every child', () => {
235
+ const hits = group().intersectLineSegment(new Vec(-10, 25), new Vec(300, 25))
236
+ expect(sortByX(hits)).toEqual([
237
+ { x: 0, y: 25 },
238
+ { x: 100, y: 25 },
239
+ { x: 200, y: 25 },
240
+ { x: 250, y: 25 },
241
+ ])
242
+ })
243
+
244
+ it('respects the filters', () => {
245
+ const g = groupWithLabel()
246
+ const A = new Vec(-10, 10)
247
+ const B = new Vec(400, 10)
248
+ expect(g.intersectLineSegment(A, B).length).toBe(6)
249
+ expect(g.intersectLineSegment(A, B, Geometry2dFilters.EXCLUDE_LABELS).length).toBe(4)
250
+ })
251
+
252
+ it('returns an empty array when nothing is hit', () => {
253
+ expect(group().intersectLineSegment(new Vec(150, -10), new Vec(150, 60))).toEqual([])
254
+ })
255
+ })
256
+
257
+ describe('Group2d.intersectCircle', () => {
258
+ it('collects the intersections from every child', () => {
259
+ const hits = group().intersectCircle(new Vec(0, 25), 10)
260
+ expect(sortByX(hits)).toEqual([
261
+ { x: 0, y: 15 },
262
+ { x: 0, y: 35 },
263
+ ])
264
+ })
265
+
266
+ it('respects the filters', () => {
267
+ const g = groupWithLabel()
268
+ expect(g.intersectCircle(new Vec(300, 10), 5).length).toBe(2)
269
+ expect(g.intersectCircle(new Vec(300, 10), 5, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
270
+ })
271
+ })
272
+
273
+ describe('Group2d.intersectPolygon and intersectPolyline', () => {
274
+ const polygon = [new Vec(50, -10), new Vec(110, -10), new Vec(110, 60), new Vec(50, 60)]
275
+
276
+ it('collects polygon intersections from every child', () => {
277
+ expect(sortByX(group().intersectPolygon(polygon))).toEqual([
278
+ { x: 50, y: 0 },
279
+ { x: 50, y: 50 },
280
+ ])
281
+ })
282
+
283
+ it('collects polyline intersections from every child', () => {
284
+ const polyline = [new Vec(50, -10), new Vec(50, 60)]
285
+ expect(sortByX(group().intersectPolyline(polyline))).toEqual([
286
+ { x: 50, y: 0 },
287
+ { x: 50, y: 50 },
288
+ ])
289
+ })
290
+
291
+ it('respects the filters', () => {
292
+ const g = groupWithLabel()
293
+ const labelPolygon = [new Vec(310, -10), new Vec(330, -10), new Vec(330, 30), new Vec(310, 30)]
294
+ expect(g.intersectPolygon(labelPolygon).length).toBe(2)
295
+ expect(g.intersectPolygon(labelPolygon, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
296
+ const labelPolyline = [new Vec(310, -10), new Vec(310, 30)]
297
+ expect(g.intersectPolyline(labelPolyline).length).toBe(2)
298
+ expect(g.intersectPolyline(labelPolyline, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
299
+ })
300
+ })
301
+
302
+ describe('Group2d.interpolateAlongEdge', () => {
303
+ it('walks through the children in order', () => {
304
+ const p = path()
305
+ expect(p.interpolateAlongEdge(0)).toMatchObject({ x: 0, y: 0 })
306
+ expect(p.interpolateAlongEdge(0.25)).toMatchObject({ x: 50, y: 0 })
307
+ expect(p.interpolateAlongEdge(0.5)).toMatchObject({ x: 100, y: 0 })
308
+ expect(p.interpolateAlongEdge(0.75)).toMatchObject({ x: 100, y: 50 })
309
+ expect(p.interpolateAlongEdge(1)).toMatchObject({ x: 100, y: 100 })
310
+ })
311
+
312
+ it('clamps to the end of the last child', () => {
313
+ expect(path().interpolateAlongEdge(1.5)).toMatchObject({ x: 100, y: 100 })
314
+ })
315
+ })
316
+
317
+ describe('Group2d.uninterpolateAlongEdge', () => {
318
+ it('is the inverse of interpolateAlongEdge', () => {
319
+ const p = path()
320
+ expect(p.uninterpolateAlongEdge(new Vec(0, 0))).toBe(0)
321
+ expect(p.uninterpolateAlongEdge(new Vec(50, 0))).toBe(0.25)
322
+ expect(p.uninterpolateAlongEdge(new Vec(100, 0))).toBe(0.5)
323
+ expect(p.uninterpolateAlongEdge(new Vec(100, 50))).toBe(0.75)
324
+ expect(p.uninterpolateAlongEdge(new Vec(100, 100))).toBe(1)
325
+ })
326
+
327
+ it('snaps points off the path to the nearest child', () => {
328
+ expect(path().uninterpolateAlongEdge(new Vec(50, -20))).toBe(0.25)
329
+ expect(path().uninterpolateAlongEdge(new Vec(120, 50))).toBe(0.75)
330
+ })
331
+ })
332
+
333
+ describe('Group2d.transform', () => {
334
+ it('transforms every child', () => {
335
+ const t = group().transform(Mat.Translate(10, 20))
336
+ expect(t).toBeInstanceOf(Group2d)
337
+ expect((t as Group2d).children.every((c) => c instanceof TransformedGeometry2d)).toBe(true)
338
+ expect(t.bounds).toMatchObject({ x: 10, y: 20, w: 250, h: 50 })
339
+ expect(t.vertices[0]).toMatchObject({ x: 10, y: 20 })
340
+ expect(t.vertices.length).toBe(8)
341
+ })
342
+
343
+ it('carries the label, debug colour and ignore flags forward', () => {
344
+ const g = new Group2d({
345
+ children: [rectA()],
346
+ isLabel: true,
347
+ debugColor: 'red',
348
+ ignore: true,
349
+ })
350
+ expect(g.transform(Mat.Identity())).toMatchObject({
351
+ isLabel: true,
352
+ debugColor: 'red',
353
+ ignore: true,
354
+ })
355
+ })
356
+
357
+ it('scales hit testing with the transform', () => {
358
+ const t = group().transform(Mat.Scale(2, 2))
359
+ expect(t.hitTestPoint(new Vec(100, 50), 0, false)).toBe(true)
360
+ expect(t.hitTestPoint(new Vec(300, 50), 0, false)).toBe(false)
361
+ expect(t.distanceToPoint(new Vec(300, 50))).toBe(100)
362
+ })
363
+
364
+ // Locks in current behaviour, see #10562.
365
+ it('drops the ignored children', () => {
366
+ const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
367
+ const g = new Group2d({ children: [rectA(), ignored] })
368
+ expect(g.ignoredChildren.length).toBe(1)
369
+ expect((g.transform(Mat.Identity()) as Group2d).ignoredChildren).toEqual([])
370
+ })
371
+ })
372
+
373
+ describe('Group2d.overlapsPolygon', () => {
374
+ it('overlaps when any child overlaps', () => {
375
+ const aroundB = [new Vec(190, -10), new Vec(260, -10), new Vec(260, 60), new Vec(190, 60)]
376
+ expect(group().overlapsPolygon(aroundB)).toBe(true)
377
+ const gap = [new Vec(120, 10), new Vec(180, 10), new Vec(180, 40), new Vec(120, 40)]
378
+ expect(group().overlapsPolygon(gap)).toBe(false)
379
+ })
380
+ })
381
+
382
+ describe('Group2d svg output', () => {
383
+ it('joins the children paths and skips labels', () => {
384
+ expect(group().getSvgPathData()).toBe('M0,0 h100 v50 h-100z M200,0 h50 v50 h-50z')
385
+ expect(groupWithLabel().getSvgPathData()).toBe('M0,0 h100 v50 h-100z M200,0 h50 v50 h-50z')
386
+ })
387
+
388
+ it('draws the children plus a small corner mark at each bounds corner', () => {
389
+ const g = new Group2d({
390
+ children: [new Rectangle2d({ width: 128, height: 64, isFilled: true })],
391
+ })
392
+ expect(g.toSimpleSvgPath()).toBe(
393
+ 'M0,0L128,0L128,64L0,64Z' +
394
+ 'M0,4 L0,0 L4,0 ' +
395
+ 'M124,0 L128,0 L128,4 ' +
396
+ 'M128,60 L128,64 L124,64 ' +
397
+ 'M4,64 L0,64 L0,60 '
398
+ )
4
399
  })
5
400
  })
@@ -1,4 +1,3 @@
1
- import { EMPTY_ARRAY } from '@tldraw/state'
2
1
  import { assert, invLerp, lerp } from '@tldraw/utils'
3
2
  import { Box } from '../Box'
4
3
  import { Mat } from '../Mat'
@@ -36,9 +35,15 @@ export class Group2d extends Geometry2d {
36
35
 
37
36
  override getVertices(filters: Geometry2dFilters): Vec[] {
38
37
  if (this.isExcludedByFilter(filters)) return []
39
- return this.children
40
- .filter((c) => !c.isExcludedByFilter(filters))
41
- .flatMap((c) => c.getVertices(filters))
38
+ const vertices: Vec[] = []
39
+ for (const child of this.children) {
40
+ if (child.isExcludedByFilter(filters)) continue
41
+ const childVertices = child.getVertices(filters)
42
+ for (let i = 0, n = childVertices.length; i < n; i++) {
43
+ vertices.push(childVertices[i])
44
+ }
45
+ }
46
+ return vertices
42
47
  }
43
48
 
44
49
  override nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
@@ -84,9 +89,11 @@ export class Group2d extends Geometry2d {
84
89
  hitInside: boolean,
85
90
  filters = Geometry2dFilters.EXCLUDE_LABELS
86
91
  ): boolean {
87
- return !!this.children
88
- .filter((c) => !c.isExcludedByFilter(filters))
89
- .find((c) => c.hitTestPoint(point, margin, hitInside))
92
+ for (const child of this.children) {
93
+ if (child.isExcludedByFilter(filters)) continue
94
+ if (child.hitTestPoint(point, margin, hitInside)) return true
95
+ }
96
+ return false
90
97
  }
91
98
 
92
99
  override hitTestLineSegment(
@@ -95,42 +102,71 @@ export class Group2d extends Geometry2d {
95
102
  zoom: number,
96
103
  filters = Geometry2dFilters.EXCLUDE_LABELS
97
104
  ): boolean {
98
- return !!this.children
99
- .filter((c) => !c.isExcludedByFilter(filters))
100
- .find((c) => c.hitTestLineSegment(A, B, zoom))
105
+ for (const child of this.children) {
106
+ if (child.isExcludedByFilter(filters)) continue
107
+ if (child.hitTestLineSegment(A, B, zoom)) return true
108
+ }
109
+ return false
101
110
  }
102
111
 
103
112
  override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
104
- return this.children.flatMap((child) => {
105
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
106
- return child.intersectLineSegment(A, B, filters)
107
- })
113
+ const result: VecLike[] = []
114
+ for (const child of this.children) {
115
+ if (child.isExcludedByFilter(filters)) continue
116
+ const hits = child.intersectLineSegment(A, B, filters)
117
+ for (let i = 0, n = hits.length; i < n; i++) {
118
+ result.push(hits[i])
119
+ }
120
+ }
121
+ return result
108
122
  }
109
123
 
110
124
  override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
111
- return this.children.flatMap((child) => {
112
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
113
- return child.intersectCircle(center, radius, filters)
114
- })
125
+ const result: VecLike[] = []
126
+ for (const child of this.children) {
127
+ if (child.isExcludedByFilter(filters)) continue
128
+ const hits = child.intersectCircle(center, radius, filters)
129
+ for (let i = 0, n = hits.length; i < n; i++) {
130
+ result.push(hits[i])
131
+ }
132
+ }
133
+ return result
115
134
  }
116
135
 
117
136
  override getBoundsVertices(): Vec[] {
118
137
  if (this.excludeFromShapeBounds) return []
119
- return this.children.flatMap((child) => child.getBoundsVertices())
138
+ const vertices: Vec[] = []
139
+ for (const child of this.children) {
140
+ const childVertices = child.getBoundsVertices()
141
+ for (let i = 0, n = childVertices.length; i < n; i++) {
142
+ vertices.push(childVertices[i])
143
+ }
144
+ }
145
+ return vertices
120
146
  }
121
147
 
122
148
  override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
123
- return this.children.flatMap((child) => {
124
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
125
- return child.intersectPolygon(polygon, filters)
126
- })
149
+ const result: VecLike[] = []
150
+ for (const child of this.children) {
151
+ if (child.isExcludedByFilter(filters)) continue
152
+ const hits = child.intersectPolygon(polygon, filters)
153
+ for (let i = 0, n = hits.length; i < n; i++) {
154
+ result.push(hits[i])
155
+ }
156
+ }
157
+ return result
127
158
  }
128
159
 
129
160
  override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {
130
- return this.children.flatMap((child) => {
131
- if (child.isExcludedByFilter(filters)) return EMPTY_ARRAY
132
- return child.intersectPolyline(polyline, filters)
133
- })
161
+ const result: VecLike[] = []
162
+ for (const child of this.children) {
163
+ if (child.isExcludedByFilter(filters)) continue
164
+ const hits = child.intersectPolyline(polyline, filters)
165
+ for (let i = 0, n = hits.length; i < n; i++) {
166
+ result.push(hits[i])
167
+ }
168
+ }
169
+ return result
134
170
  }
135
171
 
136
172
  override interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {