@tldraw/editor 3.16.0-next.6611943ca24a → 3.16.0-next.8eb6d5c2d8f4

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 (160) hide show
  1. package/dist-cjs/index.d.ts +117 -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 +6 -8
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +12 -2
  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 +75 -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 +23 -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/exports/getSvgJsx.js +34 -14
  22. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  23. package/dist-cjs/lib/hooks/useCanvasEvents.js +26 -21
  24. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  26. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  28. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  30. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/useHandleEvents.js +6 -6
  32. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  33. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  34. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  35. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  36. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  37. package/dist-cjs/lib/hooks/useSelectionEvents.js +8 -8
  38. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  39. package/dist-cjs/lib/license/LicenseManager.js +143 -53
  40. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  41. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  42. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  43. package/dist-cjs/lib/license/Watermark.js +144 -75
  44. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  45. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  46. package/dist-cjs/lib/primitives/Box.js +3 -0
  47. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  48. package/dist-cjs/lib/primitives/Vec.js +0 -4
  49. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  50. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  51. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  52. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  53. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  54. package/dist-cjs/lib/utils/dom.js.map +2 -2
  55. package/dist-cjs/lib/utils/getPointerInfo.js +2 -3
  56. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  57. package/dist-cjs/lib/utils/reparenting.js +7 -36
  58. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  59. package/dist-cjs/version.js +3 -3
  60. package/dist-cjs/version.js.map +1 -1
  61. package/dist-esm/index.d.mts +117 -109
  62. package/dist-esm/index.mjs +3 -5
  63. package/dist-esm/index.mjs.map +2 -2
  64. package/dist-esm/lib/TldrawEditor.mjs +6 -8
  65. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +12 -2
  67. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  68. package/dist-esm/lib/config/TLUserPreferences.mjs +15 -4
  69. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  70. package/dist-esm/lib/editor/Editor.mjs +75 -114
  71. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  72. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  73. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  74. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +4 -2
  75. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +11 -6
  77. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +23 -0
  79. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  80. package/dist-esm/lib/exports/getSvgJsx.mjs +34 -14
  81. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useCanvasEvents.mjs +27 -27
  83. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/useDocumentEvents.mjs +6 -6
  85. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -2
  87. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  89. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  90. package/dist-esm/lib/hooks/useHandleEvents.mjs +6 -6
  91. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  92. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  93. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  94. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  95. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  96. package/dist-esm/lib/hooks/useSelectionEvents.mjs +9 -14
  97. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  98. package/dist-esm/lib/license/LicenseManager.mjs +144 -54
  99. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  100. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  101. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  102. package/dist-esm/lib/license/Watermark.mjs +145 -76
  103. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  104. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  105. package/dist-esm/lib/primitives/Box.mjs +4 -1
  106. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  107. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  108. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  109. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  110. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  111. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  112. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  113. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  114. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -3
  115. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  116. package/dist-esm/lib/utils/reparenting.mjs +8 -41
  117. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  118. package/dist-esm/version.mjs +3 -3
  119. package/dist-esm/version.mjs.map +1 -1
  120. package/editor.css +16 -3
  121. package/package.json +7 -7
  122. package/src/index.ts +2 -9
  123. package/src/lib/TldrawEditor.tsx +7 -16
  124. package/src/lib/components/default-components/DefaultCanvas.tsx +9 -1
  125. package/src/lib/config/TLUserPreferences.ts +16 -3
  126. package/src/lib/editor/Editor.test.ts +90 -0
  127. package/src/lib/editor/Editor.ts +95 -151
  128. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  129. package/src/lib/editor/managers/FocusManager/FocusManager.ts +6 -2
  130. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +30 -8
  131. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +10 -3
  132. package/src/lib/editor/shapes/ShapeUtil.ts +46 -0
  133. package/src/lib/editor/types/misc-types.ts +0 -6
  134. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  135. package/src/lib/exports/getSvgJsx.tsx +76 -19
  136. package/src/lib/hooks/useCanvasEvents.ts +26 -26
  137. package/src/lib/hooks/useDocumentEvents.ts +6 -6
  138. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  139. package/src/lib/hooks/useGestureEvents.ts +2 -2
  140. package/src/lib/hooks/useHandleEvents.ts +6 -6
  141. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  142. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  143. package/src/lib/hooks/useSelectionEvents.ts +9 -14
  144. package/src/lib/license/LicenseManager.test.ts +721 -382
  145. package/src/lib/license/LicenseManager.ts +204 -58
  146. package/src/lib/license/LicenseProvider.tsx +74 -2
  147. package/src/lib/license/Watermark.tsx +152 -77
  148. package/src/lib/license/useLicenseManagerState.ts +2 -2
  149. package/src/lib/primitives/Box.test.ts +126 -0
  150. package/src/lib/primitives/Box.ts +10 -1
  151. package/src/lib/primitives/Vec.ts +0 -5
  152. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  153. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  154. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  155. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  156. package/src/lib/utils/dom.test.ts +103 -0
  157. package/src/lib/utils/dom.ts +8 -1
  158. package/src/lib/utils/getPointerInfo.ts +3 -2
  159. package/src/lib/utils/reparenting.ts +10 -70
  160. package/src/version.ts +3 -3
@@ -3,7 +3,7 @@ import { memo, useRef } from 'react'
3
3
  import { useCanvasEvents } from '../hooks/useCanvasEvents'
4
4
  import { useEditor } from '../hooks/useEditor'
5
5
  import { usePassThroughWheelEvents } from '../hooks/usePassThroughWheelEvents'
6
- import { preventDefault, stopEventPropagation } from '../utils/dom'
6
+ import { preventDefault } from '../utils/dom'
7
7
  import { runtime } from '../utils/runtime'
8
8
  import { watermarkDesktopSvg, watermarkMobileSvg } from '../watermarks'
9
9
  import { LicenseManager } from './LicenseManager'
@@ -28,12 +28,63 @@ export const Watermark = memo(function Watermark() {
28
28
  return (
29
29
  <>
30
30
  <LicenseStyles />
31
- <WatermarkInner src={isMobile ? WATERMARK_MOBILE_LOCAL_SRC : WATERMARK_DESKTOP_LOCAL_SRC} />
31
+ <WatermarkInner
32
+ src={isMobile ? WATERMARK_MOBILE_LOCAL_SRC : WATERMARK_DESKTOP_LOCAL_SRC}
33
+ isUnlicensed={licenseManagerState === 'unlicensed'}
34
+ />
32
35
  </>
33
36
  )
34
37
  })
35
38
 
36
- const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
39
+ const UnlicensedWatermark = memo(function UnlicensedWatermark({
40
+ isDebugMode,
41
+ isMobile,
42
+ }: {
43
+ isDebugMode: boolean
44
+ isMobile: boolean
45
+ }) {
46
+ const editor = useEditor()
47
+ const events = useCanvasEvents()
48
+ const ref = useRef<HTMLDivElement>(null)
49
+ usePassThroughWheelEvents(ref)
50
+
51
+ const url =
52
+ 'https://tldraw.dev/pricing?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
53
+
54
+ return (
55
+ <div
56
+ ref={ref}
57
+ className={LicenseManager.className}
58
+ data-debug={isDebugMode}
59
+ data-mobile={isMobile}
60
+ data-unlicensed={true}
61
+ data-testid="tl-watermark-unlicensed"
62
+ draggable={false}
63
+ {...events}
64
+ >
65
+ <button
66
+ draggable={false}
67
+ role="button"
68
+ onPointerDown={(e) => {
69
+ editor.markEventAsHandled(e)
70
+ preventDefault(e)
71
+ }}
72
+ title="The tldraw SDK requires a license key to work in production. You can get a free 100-day trial license at tldraw.dev/pricing."
73
+ onClick={() => runtime.openWindow(url, '_blank')}
74
+ >
75
+ Get a license for production
76
+ </button>
77
+ </div>
78
+ )
79
+ })
80
+
81
+ const WatermarkInner = memo(function WatermarkInner({
82
+ src,
83
+ isUnlicensed,
84
+ }: {
85
+ src: string
86
+ isUnlicensed: boolean
87
+ }) {
37
88
  const editor = useEditor()
38
89
  const isDebugMode = useValue('debug mode', () => editor.getInstanceState().isDebugMode, [editor])
39
90
  const isMobile = useValue('is mobile', () => editor.getViewportScreenBounds().width < 700, [
@@ -47,12 +98,17 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
47
98
  const maskCss = `url('${src}') center 100% / 100% no-repeat`
48
99
  const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
49
100
 
101
+ if (isUnlicensed) {
102
+ return <UnlicensedWatermark isDebugMode={isDebugMode} isMobile={isMobile} />
103
+ }
104
+
50
105
  return (
51
106
  <div
52
107
  ref={ref}
53
108
  className={LicenseManager.className}
54
109
  data-debug={isDebugMode}
55
110
  data-mobile={isMobile}
111
+ data-testid="tl-watermark-licensed"
56
112
  draggable={false}
57
113
  {...events}
58
114
  >
@@ -60,10 +116,10 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
60
116
  draggable={false}
61
117
  role="button"
62
118
  onPointerDown={(e) => {
63
- stopEventPropagation(e)
119
+ editor.markEventAsHandled(e)
64
120
  preventDefault(e)
65
121
  }}
66
- title="made with tldraw"
122
+ title="Build infinite canvas applications with the tldraw SDK. Learn more at https://tldraw.dev."
67
123
  onClick={() => runtime.openWindow(url, '_blank')}
68
124
  style={{ mask: maskCss, WebkitMask: maskCss }}
69
125
  />
@@ -75,7 +131,8 @@ const LicenseStyles = memo(function LicenseStyles() {
75
131
  const editor = useEditor()
76
132
  const className = LicenseManager.className
77
133
 
78
- const CSS = `/* ------------------- SEE LICENSE -------------------
134
+ const CSS = `
135
+ /* ------------------- SEE LICENSE -------------------
79
136
  The tldraw watermark is part of tldraw's license. It is shown for unlicensed
80
137
  or "licensed-with-watermark" users. By using this library, you agree to
81
138
  preserve the watermark's behavior, keeping it visible, unobscured, and
@@ -84,87 +141,105 @@ available to user-interaction.
84
141
  To remove the watermark, please purchase a license at tldraw.dev.
85
142
  */
86
143
 
87
- .${className} {
88
- position: absolute;
89
- bottom: var(--tl-space-2);
90
- right: var(--tl-space-2);
91
- width: 96px;
92
- height: 32px;
93
- display: flex;
94
- align-items: center;
95
- justify-content: center;
96
- z-index: var(--tl-layer-watermark) !important;
97
- background-color: color-mix(in srgb, var(--tl-color-background) 62%, transparent);
98
- opacity: 1;
99
- border-radius: 5px;
100
- pointer-events: all;
101
- padding: 2px;
102
- box-sizing: content-box;
144
+ .${className} {
145
+ position: absolute;
146
+ bottom: max(var(--tl-space-2), env(safe-area-inset-bottom));
147
+ right: max(var(--tl-space-2), env(safe-area-inset-right));
148
+ width: 96px;
149
+ height: 32px;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ z-index: var(--tl-layer-watermark) !important;
154
+ background-color: color-mix(in srgb, var(--tl-color-background) 62%, transparent);
155
+ opacity: 1;
156
+ border-radius: 5px;
157
+ pointer-events: all;
158
+ padding: 2px;
159
+ box-sizing: content-box;
160
+ }
161
+
162
+ .${className} > button {
163
+ position: absolute;
164
+ width: 96px;
165
+ height: 32px;
166
+ pointer-events: all;
167
+ cursor: inherit;
168
+ color: var(--tl-color-text);
169
+ opacity: .38;
170
+ border: 0;
171
+ padding: 0;
172
+ background-color: currentColor;
173
+ }
174
+
175
+ .${className}[data-debug='true'] {
176
+ bottom: max(46px, env(safe-area-inset-bottom));
177
+ }
178
+
179
+ .${className}[data-mobile='true'] {
180
+ border-radius: 4px 0px 0px 4px;
181
+ right: max(-2px, calc(env(safe-area-inset-right) - 2px));
182
+ width: 8px;
183
+ height: 48px;
184
+ }
185
+
186
+ .${className}[data-mobile='true'] > button {
187
+ width: 8px;
188
+ height: 32px;
189
+ }
190
+
191
+ .${className}[data-unlicensed='true'] > button {
192
+ font-size: 100px;
193
+ position: absolute;
194
+ pointer-events: all;
195
+ cursor: pointer;
196
+ color: var(--tl-color-text);
197
+ opacity: 0.8;
198
+ border: 0;
199
+ padding: 0;
200
+ background-color: transparent;
201
+ font-size: 11px;
202
+ font-weight: 600;
203
+ text-align: center;
204
+ }
205
+
206
+ .${className}[data-mobile='true'][data-unlicensed='true'] > button {
207
+ display: none;
208
+ }
209
+
210
+ @media (hover: hover) {
211
+ .${className}[data-licensed='false'] > button {
212
+ pointer-events: none;
103
213
  }
104
214
 
105
- .${className} > button {
106
- position: absolute;
107
- width: 96px;
108
- height: 32px;
109
- pointer-events: all;
110
- cursor: inherit;
111
- color: var(--tl-color-text);
112
- opacity: .38;
113
- border: 0;
114
- padding: 0;
115
- background-color: currentColor;
215
+ .${className}[data-licensed='false']:hover {
216
+ background-color: var(--tl-color-background);
217
+ transition: background-color 0.2s ease-in-out;
218
+ transition-delay: 0.32s;
116
219
  }
117
220
 
118
- .${className}[data-debug='true'] {
119
- bottom: 46px;
221
+ .${className}[data-licensed='false']:hover > button {
222
+ animation: ${className}_delayed_link 0.2s forwards ease-in-out;
223
+ animation-delay: 0.32s;
120
224
  }
121
225
 
122
- .${className}[data-mobile='true'] {
123
- border-radius: 4px 0px 0px 4px;
124
- right: -2px;
125
- width: 8px;
126
- height: 48px;
226
+ .${className}[data-licensed='false'] > button:focus-visible {
227
+ opacity: 1;
127
228
  }
229
+ }
128
230
 
129
- .${className}[data-mobile='true'] > button {
130
- width: 8px;
131
- height: 32px;
231
+ @keyframes ${className}_delayed_link {
232
+ 0% {
233
+ cursor: inherit;
234
+ opacity: .38;
235
+ pointer-events: none;
132
236
  }
133
-
134
- @media (hover: hover) {
135
- .${className} > button {
136
- pointer-events: none;
137
- }
138
-
139
- .${className}:hover {
140
- background-color: var(--tl-color-background);
141
- transition: background-color 0.2s ease-in-out;
142
- transition-delay: 0.32s;
143
- }
144
-
145
- .${className}:hover > button {
146
- animation: ${className}_delayed_link 0.2s forwards ease-in-out;
147
- animation-delay: 0.32s;
148
- }
149
-
150
- .${className} > button:focus-visible {
151
- opacity: 1;
152
- }
237
+ 100% {
238
+ cursor: pointer;
239
+ opacity: 1;
240
+ pointer-events: all;
153
241
  }
154
-
155
-
156
- @keyframes ${className}_delayed_link {
157
- 0% {
158
- cursor: inherit;
159
- opacity: .38;
160
- pointer-events: none;
161
- }
162
- 100% {
163
- cursor: pointer;
164
- opacity: 1;
165
- pointer-events: all;
166
- }
167
- }`
242
+ }`
168
243
 
169
244
  return <style nonce={editor.options.nonce}>{CSS}</style>
170
245
  })
@@ -1,7 +1,7 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { LicenseManager } from './LicenseManager'
2
+ import { LicenseManager, LicenseState } from './LicenseManager'
3
3
 
4
4
  /** @internal */
5
- export function useLicenseManagerState(licenseManager: LicenseManager) {
5
+ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseState {
6
6
  return useValue('watermarkState', () => licenseManager.state.get(), [licenseManager])
7
7
  }
@@ -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)