@tldraw/editor 3.16.0-canary.ffdf566dd0a8 → 3.16.0-internal.71f83a8a571b

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 (218) hide show
  1. package/dist-cjs/index.d.ts +137 -126
  2. package/dist-cjs/index.js +6 -6
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +8 -8
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +7 -10
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +14 -23
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
  14. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
  19. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  20. package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
  21. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  22. package/dist-cjs/lib/editor/Editor.js +67 -128
  23. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  24. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  25. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  26. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -1
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/BaseBoxShapeUtil.js.map +1 -1
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +23 -0
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  32. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.js.map +2 -2
  33. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  34. package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
  35. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  36. package/dist-cjs/lib/hooks/useCanvasEvents.js +44 -35
  37. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  38. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  39. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  40. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  41. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  43. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  44. package/dist-cjs/lib/hooks/useHandleEvents.js +3 -3
  45. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  46. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  47. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  48. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  49. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  50. package/dist-cjs/lib/hooks/useSelectionEvents.js +4 -4
  51. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  52. package/dist-cjs/lib/license/LicenseManager.js +140 -53
  53. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  54. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  55. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  56. package/dist-cjs/lib/license/Watermark.js +73 -11
  57. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  58. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  59. package/dist-cjs/lib/options.js +6 -0
  60. package/dist-cjs/lib/options.js.map +2 -2
  61. package/dist-cjs/lib/primitives/Box.js +3 -0
  62. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  63. package/dist-cjs/lib/primitives/Vec.js +0 -4
  64. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  65. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  66. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  67. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  68. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  69. package/dist-cjs/lib/utils/dom.js +12 -1
  70. package/dist-cjs/lib/utils/dom.js.map +2 -2
  71. package/dist-cjs/lib/utils/getPointerInfo.js +2 -2
  72. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  73. package/dist-cjs/lib/utils/reparenting.js +2 -35
  74. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  75. package/dist-cjs/version.js +3 -3
  76. package/dist-cjs/version.js.map +1 -1
  77. package/dist-esm/index.d.mts +137 -126
  78. package/dist-esm/index.mjs +9 -7
  79. package/dist-esm/index.mjs.map +2 -2
  80. package/dist-esm/lib/TldrawEditor.mjs +9 -9
  81. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  82. package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
  83. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  84. package/dist-esm/lib/components/Shape.mjs +7 -10
  85. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  86. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +15 -24
  87. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  88. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  89. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
  90. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  91. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  92. package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
  93. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  94. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
  95. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  96. package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
  97. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  98. package/dist-esm/lib/editor/Editor.mjs +67 -128
  99. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  100. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  101. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  102. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  103. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -1
  104. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  105. package/dist-esm/lib/editor/shapes/BaseBoxShapeUtil.mjs.map +1 -1
  106. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +23 -0
  107. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  108. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.mjs.map +2 -2
  109. package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
  110. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  111. package/dist-esm/lib/hooks/useCanvasEvents.mjs +47 -37
  112. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  113. package/dist-esm/lib/hooks/useDocumentEvents.mjs +11 -6
  114. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  115. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
  116. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  117. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  118. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  119. package/dist-esm/lib/hooks/useHandleEvents.mjs +9 -4
  120. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  121. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  122. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  123. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  124. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  125. package/dist-esm/lib/hooks/useSelectionEvents.mjs +6 -5
  126. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  127. package/dist-esm/lib/license/LicenseManager.mjs +141 -54
  128. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  129. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  130. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  131. package/dist-esm/lib/license/Watermark.mjs +74 -12
  132. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  133. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  134. package/dist-esm/lib/options.mjs +6 -0
  135. package/dist-esm/lib/options.mjs.map +2 -2
  136. package/dist-esm/lib/primitives/Box.mjs +4 -1
  137. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  138. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  139. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  140. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  141. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  142. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  143. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  144. package/dist-esm/lib/utils/dom.mjs +12 -1
  145. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  146. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -2
  147. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  148. package/dist-esm/lib/utils/reparenting.mjs +3 -40
  149. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  150. package/dist-esm/version.mjs +3 -3
  151. package/dist-esm/version.mjs.map +1 -1
  152. package/editor.css +308 -292
  153. package/package.json +14 -37
  154. package/src/index.ts +4 -9
  155. package/src/lib/TldrawEditor.tsx +14 -21
  156. package/src/lib/components/MenuClickCapture.tsx +0 -8
  157. package/src/lib/components/Shape.tsx +6 -12
  158. package/src/lib/components/default-components/DefaultCanvas.tsx +12 -23
  159. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  160. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  161. package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
  162. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
  163. package/src/lib/config/TLUserPreferences.ts +1 -1
  164. package/src/lib/editor/Editor.test.ts +108 -11
  165. package/src/lib/editor/Editor.ts +112 -195
  166. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  167. package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
  168. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  169. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  170. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  171. package/src/lib/editor/managers/FontManager/FontManager.test.ts +38 -27
  172. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  173. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  174. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  175. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  176. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  177. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -26
  178. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
  179. package/src/lib/editor/shapes/BaseBoxShapeUtil.tsx +2 -2
  180. package/src/lib/editor/shapes/ShapeUtil.ts +51 -8
  181. package/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts +2 -1
  182. package/src/lib/editor/types/misc-types.ts +0 -6
  183. package/src/lib/exports/getSvgJsx.test.ts +874 -0
  184. package/src/lib/exports/getSvgJsx.tsx +78 -21
  185. package/src/lib/hooks/useCanvasEvents.ts +60 -47
  186. package/src/lib/hooks/useDocumentEvents.ts +11 -6
  187. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -2
  188. package/src/lib/hooks/useGestureEvents.ts +2 -2
  189. package/src/lib/hooks/useHandleEvents.ts +9 -4
  190. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  191. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  192. package/src/lib/hooks/useSelectionEvents.ts +6 -5
  193. package/src/lib/license/LicenseManager.test.ts +724 -383
  194. package/src/lib/license/LicenseManager.ts +201 -58
  195. package/src/lib/license/LicenseProvider.tsx +74 -2
  196. package/src/lib/license/Watermark.test.tsx +2 -1
  197. package/src/lib/license/Watermark.tsx +79 -12
  198. package/src/lib/license/useLicenseManagerState.ts +2 -2
  199. package/src/lib/options.ts +6 -0
  200. package/src/lib/primitives/Box.test.ts +126 -0
  201. package/src/lib/primitives/Box.ts +10 -1
  202. package/src/lib/primitives/Vec.ts +0 -5
  203. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  204. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  205. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  206. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  207. package/src/lib/utils/dom.test.ts +94 -0
  208. package/src/lib/utils/dom.ts +38 -1
  209. package/src/lib/utils/getPointerInfo.ts +2 -1
  210. package/src/lib/utils/reparenting.ts +3 -69
  211. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  212. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  213. package/src/version.ts +3 -3
  214. package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
  215. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  216. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  217. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  218. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -510,6 +510,132 @@ describe('Box', () => {
510
510
  })
511
511
  })
512
512
 
513
+ describe('Box.ContainsApproximately', () => {
514
+ it('returns true when first box exactly contains second', () => {
515
+ const boxA = new Box(0, 0, 100, 100)
516
+ const boxB = new Box(10, 10, 50, 50)
517
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
518
+ })
519
+
520
+ it('returns false when first box clearly does not contain second', () => {
521
+ const boxA = new Box(0, 0, 50, 50)
522
+ const boxB = new Box(10, 10, 100, 100)
523
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
524
+ })
525
+
526
+ it('returns true when containment is within default precision tolerance', () => {
527
+ // Box B extends very slightly outside A (within floating-point precision)
528
+ const boxA = new Box(0, 0, 100, 100)
529
+ const boxB = new Box(10, 10, 80, 80)
530
+ // Move B's max edges just slightly outside A's bounds
531
+ boxB.w = 90.000000000001 // maxX = 100.000000000001 (slightly beyond 100)
532
+ boxB.h = 90.000000000001 // maxY = 100.000000000001 (slightly beyond 100)
533
+
534
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
535
+ expect(Box.Contains(boxA, boxB)).toBe(false) // strict contains would fail
536
+ })
537
+
538
+ it('returns false when containment exceeds default precision tolerance', () => {
539
+ const boxA = new Box(0, 0, 100, 100)
540
+ const boxB = new Box(10, 10, 80, 80)
541
+ // Move B's max edges clearly outside A's bounds
542
+ boxB.w = 95 // maxX = 105 (clearly beyond 100)
543
+ boxB.h = 95 // maxY = 105 (clearly beyond 100)
544
+
545
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
546
+ })
547
+
548
+ it('respects custom precision parameter', () => {
549
+ const boxA = new Box(0, 0, 100, 100)
550
+ const boxB = new Box(10, 10, 85, 85) // maxX=95, maxY=95
551
+
552
+ // With loose precision (10), should contain (95 is within 100-10=90 tolerance)
553
+ expect(Box.ContainsApproximately(boxA, boxB, 10)).toBe(true)
554
+
555
+ // With tight precision (4), should still contain (95 is within 100-4=96)
556
+ expect(Box.ContainsApproximately(boxA, boxB, 4)).toBe(true)
557
+
558
+ // Since 95 < 100, the precision parameter doesn't affect containment here
559
+ expect(Box.ContainsApproximately(boxA, boxB, 4.9)).toBe(true)
560
+ })
561
+
562
+ it('handles negative coordinates correctly', () => {
563
+ const boxA = new Box(-50, -50, 100, 100) // bounds: (-50,-50) to (50,50)
564
+ const boxB = new Box(-40, -40, 79.999999999, 79.999999999) // bounds: (-40,-40) to (39.999999999, 39.999999999)
565
+
566
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
567
+ })
568
+
569
+ it('handles edge case where boxes are identical', () => {
570
+ const boxA = new Box(10, 20, 100, 200)
571
+ const boxB = new Box(10, 20, 100, 200)
572
+
573
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
574
+ })
575
+
576
+ it('handles edge case where inner box touches outer box edges', () => {
577
+ const boxA = new Box(0, 0, 100, 100)
578
+ const boxB = new Box(0, 0, 100, 100) // exactly the same
579
+
580
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
581
+
582
+ // Slightly smaller inner box
583
+ const boxC = new Box(0.000001, 0.000001, 99.999998, 99.999998)
584
+ expect(Box.ContainsApproximately(boxA, boxC)).toBe(true)
585
+ })
586
+
587
+ it('handles floating-point precision issues in real-world scenarios', () => {
588
+ // Simulate common floating-point arithmetic issues
589
+ const containerBox = new Box(0, 0, 100, 100)
590
+
591
+ // Box that should be contained but has floating-point errors
592
+ const innerBox = new Box(10, 10, 80, 80)
593
+ // Simulate floating-point arithmetic that results in tiny overruns
594
+ innerBox.w = 90.00000000000001 // maxX = 100.00000000000001 (tiny overrun)
595
+ innerBox.h = 90.00000000000001 // maxY = 100.00000000000001 (tiny overrun)
596
+
597
+ expect(Box.ContainsApproximately(containerBox, innerBox)).toBe(true)
598
+ expect(Box.Contains(containerBox, innerBox)).toBe(false) // strict contains fails due to precision
599
+ })
600
+
601
+ it('fails when any edge exceeds tolerance', () => {
602
+ const boxA = new Box(10, 10, 100, 100) // bounds: (10,10) to (110,110)
603
+
604
+ // Test each edge exceeding tolerance
605
+ const testCases = [
606
+ { name: 'left edge', box: new Box(5, 20, 80, 80) }, // minX too small
607
+ { name: 'top edge', box: new Box(20, 5, 80, 80) }, // minY too small
608
+ { name: 'right edge', box: new Box(20, 20, 95, 80) }, // maxX too large (20+95=115 > 110)
609
+ { name: 'bottom edge', box: new Box(20, 20, 80, 95) }, // maxY too large (20+95=115 > 110)
610
+ ]
611
+
612
+ testCases.forEach(({ box }) => {
613
+ expect(Box.ContainsApproximately(boxA, box, 1)).toBe(false) // tight precision
614
+ })
615
+ })
616
+
617
+ it('works with zero-sized dimensions', () => {
618
+ const boxA = new Box(0, 0, 100, 100)
619
+ const boxB = new Box(50, 50, 0, 0) // zero-sized box (point)
620
+
621
+ expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
622
+ })
623
+
624
+ it('handles precision parameter edge cases', () => {
625
+ const boxA = new Box(0, 0, 100, 100)
626
+ const boxB = new Box(10, 10, 91, 91) // maxX=101, maxY=101 (clearly outside)
627
+
628
+ // Zero precision should work like strict Contains
629
+ expect(Box.ContainsApproximately(boxA, boxB, 0)).toBe(false)
630
+
631
+ // Small precision should still fail (101 > 100)
632
+ expect(Box.ContainsApproximately(boxA, boxB, 0.5)).toBe(false)
633
+
634
+ // Sufficient precision should succeed (101 <= 100 + 2)
635
+ expect(Box.ContainsApproximately(boxA, boxB, 2)).toBe(true)
636
+ })
637
+ })
638
+
513
639
  describe('Box.Includes', () => {
514
640
  it('returns true when boxes collide or contain', () => {
515
641
  const boxA = new Box(0, 0, 50, 50)
@@ -1,6 +1,6 @@
1
1
  import { BoxModel } from '@tldraw/tlschema'
2
2
  import { Vec, VecLike } from './Vec'
3
- import { PI, PI2, toPrecision } from './utils'
3
+ import { approximatelyLte, PI, PI2, toPrecision } from './utils'
4
4
 
5
5
  /** @public */
6
6
  export type BoxLike = BoxModel | Box
@@ -417,6 +417,15 @@ export class Box {
417
417
  return A.minX < B.minX && A.minY < B.minY && A.maxY > B.maxY && A.maxX > B.maxX
418
418
  }
419
419
 
420
+ static ContainsApproximately(A: Box, B: Box, precision?: number) {
421
+ return (
422
+ approximatelyLte(A.minX, B.minX, precision) &&
423
+ approximatelyLte(A.minY, B.minY, precision) &&
424
+ approximatelyLte(B.maxX, A.maxX, precision) &&
425
+ approximatelyLte(B.maxY, A.maxY, precision)
426
+ )
427
+ }
428
+
420
429
  static Includes(A: Box, B: Box) {
421
430
  return Box.Collides(A, B) || Box.Contains(A, B)
422
431
  }
@@ -240,11 +240,6 @@ export class Vec {
240
240
  return Vec.EqualsXY(this, x, y)
241
241
  }
242
242
 
243
- /** @deprecated use `uni` instead */
244
- norm() {
245
- return this.uni()
246
- }
247
-
248
243
  toFixed() {
249
244
  this.x = toFixed(this.x)
250
245
  this.y = toFixed(this.y)
@@ -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)