@tldraw/editor 3.16.0-canary.5170ef6b6e20 → 3.16.0-canary.54408756ac9c

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 (77) hide show
  1. package/dist-cjs/index.d.ts +13 -101
  2. package/dist-cjs/index.js +3 -5
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +1 -5
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +3 -99
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  9. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  10. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
  11. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  12. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  13. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  14. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  15. package/dist-cjs/lib/license/LicenseManager.js +110 -35
  16. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  17. package/dist-cjs/lib/license/LicenseProvider.js +36 -3
  18. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  19. package/dist-cjs/lib/license/Watermark.js +68 -6
  20. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  21. package/dist-cjs/lib/primitives/Vec.js +0 -4
  22. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  23. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
  24. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  25. package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
  26. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  27. package/dist-cjs/lib/utils/reparenting.js +2 -35
  28. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  29. package/dist-cjs/version.js +3 -3
  30. package/dist-cjs/version.js.map +1 -1
  31. package/dist-esm/index.d.mts +13 -101
  32. package/dist-esm/index.mjs +3 -5
  33. package/dist-esm/index.mjs.map +2 -2
  34. package/dist-esm/lib/TldrawEditor.mjs +1 -5
  35. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  36. package/dist-esm/lib/editor/Editor.mjs +3 -99
  37. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  38. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  39. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  40. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
  41. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  42. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  43. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  44. package/dist-esm/lib/license/LicenseManager.mjs +111 -36
  45. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  46. package/dist-esm/lib/license/LicenseProvider.mjs +36 -4
  47. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  48. package/dist-esm/lib/license/Watermark.mjs +68 -6
  49. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  50. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  51. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  52. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
  53. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  54. package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
  55. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  56. package/dist-esm/lib/utils/reparenting.mjs +3 -40
  57. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  58. package/dist-esm/version.mjs +3 -3
  59. package/dist-esm/version.mjs.map +1 -1
  60. package/editor.css +8 -3
  61. package/package.json +7 -7
  62. package/src/index.ts +1 -9
  63. package/src/lib/TldrawEditor.tsx +1 -13
  64. package/src/lib/editor/Editor.ts +1 -125
  65. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  66. package/src/lib/editor/shapes/ShapeUtil.ts +11 -0
  67. package/src/lib/editor/types/misc-types.ts +0 -6
  68. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  69. package/src/lib/license/LicenseManager.test.ts +643 -387
  70. package/src/lib/license/LicenseManager.ts +156 -44
  71. package/src/lib/license/LicenseProvider.tsx +69 -5
  72. package/src/lib/license/Watermark.tsx +73 -6
  73. package/src/lib/primitives/Vec.ts +0 -5
  74. package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
  75. package/src/lib/primitives/geometry/Group2d.ts +4 -0
  76. package/src/lib/utils/reparenting.ts +3 -69
  77. package/src/version.ts +3 -3
@@ -1,16 +1,16 @@
1
1
  import { atom } from '@tldraw/state'
2
- import { fetch } from '@tldraw/utils'
3
- import { publishDates } from '../../version'
2
+ import { publishDates, version } from '../../version'
4
3
  import { getDefaultCdnBaseUrl } from '../utils/assets'
5
4
  import { importPublicKey, str2ab } from '../utils/licensing'
6
5
 
7
- const GRACE_PERIOD_DAYS = 5
6
+ const GRACE_PERIOD_DAYS = 30
8
7
 
9
8
  export const FLAGS = {
10
- ANNUAL_LICENSE: 0x1,
11
- PERPETUAL_LICENSE: 0x2,
12
- INTERNAL_LICENSE: 0x4,
13
- WITH_WATERMARK: 0x8,
9
+ ANNUAL_LICENSE: 1,
10
+ PERPETUAL_LICENSE: 1 << 1,
11
+ INTERNAL_LICENSE: 1 << 2,
12
+ WITH_WATERMARK: 1 << 3,
13
+ EVALUATION_LICENSE: 1 << 4,
14
14
  }
15
15
  const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
16
16
 
@@ -36,11 +36,12 @@ export interface LicenseInfo {
36
36
 
37
37
  /** @internal */
38
38
  export type LicenseState =
39
- | 'pending'
40
- | 'licensed'
41
- | 'licensed-with-watermark'
42
- | 'unlicensed'
43
- | 'internal-expired'
39
+ | 'pending' // License validation is in progress
40
+ | 'licensed' // License is valid and active (no restrictions)
41
+ | 'licensed-with-watermark' // License is valid but shows watermark (evaluation licenses, WITH_WATERMARK licenses)
42
+ | 'unlicensed' // No valid license found or license is invalid (development)
43
+ | 'unlicensed-production' // No valid license in production deployment (missing, invalid, or wrong domain)
44
+ | 'expired' // License has been expired (30 days past expiration for regular licenses, immediately for evaluation licenses)
44
45
  /** @internal */
45
46
  export type InvalidLicenseReason =
46
47
  | 'invalid-license-key'
@@ -69,11 +70,17 @@ export interface ValidLicenseKeyResult {
69
70
  isPerpetualLicenseExpired: boolean
70
71
  isInternalLicense: boolean
71
72
  isLicensedWithWatermark: boolean
73
+ isEvaluationLicense: boolean
74
+ isEvaluationLicenseExpired: boolean
75
+ daysSinceExpiry: number
72
76
  }
73
77
 
74
78
  /** @internal */
75
79
  export type TestEnvironment = 'development' | 'production'
76
80
 
81
+ /** @internal */
82
+ export type TrackType = 'unlicensed' | 'with_watermark' | 'evaluation' | null
83
+
77
84
  /** @internal */
78
85
  export class LicenseManager {
79
86
  private publicKey =
@@ -96,11 +103,13 @@ export class LicenseManager {
96
103
 
97
104
  this.getLicenseFromKey(licenseKey)
98
105
  .then((result) => {
99
- const licenseState = getLicenseState(result)
106
+ const licenseState = getLicenseState(
107
+ result,
108
+ (messages: string[]) => this.outputMessages(messages),
109
+ this.isDevelopment
110
+ )
100
111
 
101
- if (!this.isDevelopment && licenseState === 'unlicensed') {
102
- fetch(WATERMARK_TRACK_SRC)
103
- }
112
+ this.maybeTrack(result, licenseState)
104
113
 
105
114
  this.state.set(licenseState)
106
115
  })
@@ -121,6 +130,47 @@ export class LicenseManager {
121
130
  )
122
131
  }
123
132
 
133
+ private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
134
+ // Track watermark for unlicensed production deployments
135
+ if (licenseState === 'unlicensed-production') {
136
+ return 'unlicensed'
137
+ }
138
+
139
+ if (this.isDevelopment) {
140
+ return null
141
+ }
142
+
143
+ if (!result.isLicenseParseable) {
144
+ return null
145
+ }
146
+
147
+ // Track evaluation licenses (for analytics, even though no watermark is shown)
148
+ if (result.isEvaluationLicense) {
149
+ return 'evaluation'
150
+ }
151
+
152
+ // Track licenses that show watermarks
153
+ if (licenseState === 'licensed-with-watermark') {
154
+ return 'with_watermark'
155
+ }
156
+
157
+ return null
158
+ }
159
+
160
+ private maybeTrack(result: LicenseFromKeyResult, licenseState: LicenseState): void {
161
+ const trackType = this.getTrackType(result, licenseState)
162
+ if (!trackType) {
163
+ return
164
+ }
165
+
166
+ const url = new URL(WATERMARK_TRACK_SRC)
167
+ url.searchParams.set('version', version)
168
+ url.searchParams.set('license_type', trackType)
169
+
170
+ // eslint-disable-next-line no-restricted-globals
171
+ fetch(url.toString())
172
+ }
173
+
124
174
  private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
125
175
  const [data, signature] = licenseKey.split('.')
126
176
  const [prefix, encodedData] = data.split('/')
@@ -207,6 +257,9 @@ export class LicenseManager {
207
257
  const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
208
258
  const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
209
259
 
260
+ const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
261
+ const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
262
+
210
263
  const result: ValidLicenseKeyResult = {
211
264
  license: licenseInfo,
212
265
  isLicenseParseable: true,
@@ -219,6 +272,10 @@ export class LicenseManager {
219
272
  isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
220
273
  isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
221
274
  isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
275
+ isEvaluationLicense,
276
+ isEvaluationLicenseExpired:
277
+ isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
278
+ daysSinceExpiry,
222
279
  }
223
280
  this.outputLicenseInfoIfNeeded(result)
224
281
 
@@ -284,15 +341,7 @@ export class LicenseManager {
284
341
 
285
342
  private isAnnualLicenseExpired(expiryDate: Date) {
286
343
  const expiration = this.getExpirationDateWithGracePeriod(expiryDate)
287
- const isExpired = new Date() >= expiration
288
- // If it is not expired yet (including the grace period), but after the expiry date we warn the users
289
- if (!isExpired && new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {
290
- this.outputMessages([
291
- 'tldraw license is about to expire, you are in a grace period.',
292
- `Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`,
293
- ])
294
- }
295
- return isExpired
344
+ return new Date() >= expiration
296
345
  }
297
346
 
298
347
  private isPerpetualLicenseExpired(expiryDate: Date) {
@@ -305,6 +354,21 @@ export class LicenseManager {
305
354
  return dates.major >= expiration || dates.minor >= expiration
306
355
  }
307
356
 
357
+ private getDaysSinceExpiry(expiryDate: Date): number {
358
+ const now = new Date()
359
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
360
+ const diffTime = now.getTime() - expiration.getTime()
361
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
362
+ return Math.max(0, diffDays)
363
+ }
364
+
365
+ private isEvaluationLicenseExpired(expiryDate: Date): boolean {
366
+ // Evaluation licenses have no grace period - they expire immediately
367
+ const now = new Date()
368
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
369
+ return now >= expiration
370
+ }
371
+
308
372
  private isFlagEnabled(flags: number, flag: number) {
309
373
  return (flags & flag) === flag
310
374
  }
@@ -322,19 +386,6 @@ export class LicenseManager {
322
386
  }
323
387
 
324
388
  private outputLicenseInfoIfNeeded(result: ValidLicenseKeyResult) {
325
- if (result.isAnnualLicenseExpired) {
326
- this.outputMessages([
327
- 'Your tldraw license has expired!',
328
- `Please reach out to ${LICENSE_EMAIL} to renew.`,
329
- ])
330
- }
331
-
332
- if (!result.isDomainValid && !result.isDevelopment) {
333
- this.outputMessages([
334
- 'This tldraw license key is not valid for this domain!',
335
- `Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`,
336
- ])
337
- }
338
389
  // If we added a new flag it will be twice the value of the currently highest flag.
339
390
  // And if all the current flags are on we would get the `HIGHEST_FLAG * 2 - 1`, so anything higher than that means there are new flags.
340
391
  if (result.license.flags >= HIGHEST_FLAG * 2) {
@@ -371,13 +422,74 @@ export class LicenseManager {
371
422
  static className = 'tl-watermark_SEE-LICENSE'
372
423
  }
373
424
 
374
- export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
375
- if (!result.isLicenseParseable) return 'unlicensed'
376
- if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
425
+ export function getLicenseState(
426
+ result: LicenseFromKeyResult,
427
+ outputMessages: (messages: string[]) => void,
428
+ isDevelopment: boolean
429
+ ): LicenseState {
430
+ if (!result.isLicenseParseable) {
431
+ if (isDevelopment) {
432
+ return 'unlicensed'
433
+ }
434
+
435
+ // All unlicensed scenarios should not work in production
436
+ if (result.reason === 'no-key-provided') {
437
+ outputMessages([
438
+ 'No tldraw license key provided!',
439
+ 'A license is required for production deployments.',
440
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
441
+ ])
442
+ } else {
443
+ outputMessages([
444
+ 'Invalid license key. tldraw requires a valid license for production use.',
445
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
446
+ ])
447
+ }
448
+ return 'unlicensed-production'
449
+ }
450
+
451
+ if (!result.isDomainValid && !result.isDevelopment) {
452
+ outputMessages([
453
+ 'License key is not valid for this domain.',
454
+ 'A license is required for production deployments.',
455
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
456
+ ])
457
+ return 'unlicensed-production'
458
+ }
459
+
460
+ // Handle evaluation licenses - they expire immediately with no grace period
461
+ if (result.isEvaluationLicense) {
462
+ if (result.isEvaluationLicenseExpired) {
463
+ outputMessages([
464
+ 'Your tldraw evaluation license has expired!',
465
+ `Please reach out to ${LICENSE_EMAIL} to purchase a full license.`,
466
+ ])
467
+ return 'expired'
468
+ } else {
469
+ // Valid evaluation license - tracked but no watermark shown
470
+ return 'licensed'
471
+ }
472
+ }
473
+
474
+ // Handle expired regular licenses (both annual and perpetual)
377
475
  if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
378
- // Check if it's an expired internal license with valid domain
379
- const internalExpired = result.isInternalLicense && result.isDomainValid
380
- return internalExpired ? 'internal-expired' : 'unlicensed'
476
+ outputMessages([
477
+ 'Your tldraw license has been expired for more than 30 days!',
478
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
479
+ ])
480
+ return 'expired'
481
+ }
482
+
483
+ // Check if license is past expiry date but within grace period
484
+ const daysSinceExpiry = result.daysSinceExpiry
485
+ if (daysSinceExpiry > 0 && !result.isEvaluationLicense) {
486
+ outputMessages([
487
+ 'Your tldraw license has expired.',
488
+ `License expired ${daysSinceExpiry} days ago.`,
489
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
490
+ ])
491
+ // Within 30-day grace period: still licensed (no watermark)
492
+ return 'licensed'
381
493
  }
382
494
 
383
495
  // License is valid, determine if it has watermark
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { createContext, ReactNode, useContext, useState } from 'react'
2
+ import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
3
3
  import { LicenseManager } from './LicenseManager'
4
4
 
5
5
  /** @internal */
@@ -8,9 +8,16 @@ export const LicenseContext = createContext({} as LicenseManager)
8
8
  /** @internal */
9
9
  export const useLicenseContext = () => useContext(LicenseContext)
10
10
 
11
+ function shouldHideEditorAfterDelay(licenseState: string): boolean {
12
+ return licenseState === 'expired' || licenseState === 'unlicensed-production'
13
+ }
14
+
15
+ /** @internal */
16
+ export const LICENSE_TIMEOUT = 5000
17
+
11
18
  /** @internal */
12
19
  export function LicenseProvider({
13
- licenseKey,
20
+ licenseKey = getLicenseKeyFromEnv() ?? undefined,
14
21
  children,
15
22
  }: {
16
23
  licenseKey?: string
@@ -18,11 +25,68 @@ export function LicenseProvider({
18
25
  }) {
19
26
  const [licenseManager] = useState(() => new LicenseManager(licenseKey))
20
27
  const licenseState = useValue(licenseManager.state)
28
+ const [showEditor, setShowEditor] = useState(true)
21
29
 
22
- // If internal license has expired, don't render the editor at all
23
- if (licenseState === 'internal-expired') {
24
- return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
30
+ // When license expires or no license in production, show for 5 seconds then hide
31
+ useEffect(() => {
32
+ if (shouldHideEditorAfterDelay(licenseState) && showEditor) {
33
+ // eslint-disable-next-line no-restricted-globals
34
+ const timer = setTimeout(() => {
35
+ setShowEditor(false)
36
+ }, LICENSE_TIMEOUT)
37
+
38
+ return () => clearTimeout(timer)
39
+ }
40
+ }, [licenseState, showEditor])
41
+
42
+ // If license is expired or no license in production and 5 seconds have passed, don't render anything (blank screen)
43
+ if (shouldHideEditorAfterDelay(licenseState) && !showEditor) {
44
+ return <LicenseGate />
25
45
  }
26
46
 
27
47
  return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
28
48
  }
49
+
50
+ // Renders as a hidden div that can be detected by tests
51
+ function LicenseGate() {
52
+ return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
53
+ }
54
+
55
+ let envLicenseKey: string | undefined | null = undefined
56
+ function getLicenseKeyFromEnv() {
57
+ if (envLicenseKey !== undefined) {
58
+ return envLicenseKey
59
+ }
60
+ // it's important here that we write out the full process.env.WHATEVER expression instead of
61
+ // doing something like process.env[someVariable]. This is because most bundlers do something
62
+ // like a find-replace inject environment variables, and so won't pick up on dynamic ones. It
63
+ // also means we can't do checks like `process.env && process.env.WHATEVER`, which is why we use
64
+ // the `getEnv` try/catch approach.
65
+
66
+ // framework-specific prefixes borrowed from the ones vercel uses, but trimmed down to just the
67
+ // react-y ones: https://vercel.com/docs/environment-variables/framework-environment-variables
68
+ envLicenseKey =
69
+ getEnv(() => process.env.TLDRAW_LICENSE_KEY) ||
70
+ getEnv(() => process.env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
71
+ getEnv(() => process.env.REACT_APP_TLDRAW_LICENSE_KEY) ||
72
+ getEnv(() => process.env.GATSBY_TLDRAW_LICENSE_KEY) ||
73
+ getEnv(() => process.env.VITE_TLDRAW_LICENSE_KEY) ||
74
+ getEnv(() => process.env.PUBLIC_TLDRAW_LICENSE_KEY) ||
75
+ getEnv(() => (import.meta as any).env.TLDRAW_LICENSE_KEY) ||
76
+ getEnv(() => (import.meta as any).env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
77
+ getEnv(() => (import.meta as any).env.REACT_APP_TLDRAW_LICENSE_KEY) ||
78
+ getEnv(() => (import.meta as any).env.GATSBY_TLDRAW_LICENSE_KEY) ||
79
+ getEnv(() => (import.meta as any).env.VITE_TLDRAW_LICENSE_KEY) ||
80
+ getEnv(() => (import.meta as any).env.PUBLIC_TLDRAW_LICENSE_KEY) ||
81
+ null
82
+
83
+ return envLicenseKey
84
+ }
85
+
86
+ function getEnv(cb: () => string | undefined) {
87
+ try {
88
+ return cb()
89
+ } catch {
90
+ return undefined
91
+ }
92
+ }
@@ -28,12 +28,74 @@ 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 events = useCanvasEvents()
47
+ const ref = useRef<HTMLDivElement>(null)
48
+ usePassThroughWheelEvents(ref)
49
+
50
+ const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
51
+
52
+ return (
53
+ <div
54
+ ref={ref}
55
+ className={LicenseManager.className}
56
+ data-debug={isDebugMode}
57
+ data-mobile={isMobile}
58
+ data-unlicensed={true}
59
+ data-testid="tl-watermark-unlicensed"
60
+ draggable={false}
61
+ {...events}
62
+ >
63
+ <button
64
+ draggable={false}
65
+ role="button"
66
+ onPointerDown={(e) => {
67
+ stopEventPropagation(e)
68
+ preventDefault(e)
69
+ }}
70
+ title="Unlicensed - click to get a license"
71
+ onClick={() => runtime.openWindow(url, '_blank')}
72
+ style={{
73
+ position: 'absolute',
74
+ pointerEvents: 'all',
75
+ cursor: 'pointer',
76
+ color: 'var(--tl-color-text)',
77
+ opacity: 0.8,
78
+ border: 0,
79
+ padding: 0,
80
+ backgroundColor: 'transparent',
81
+ fontSize: '11px',
82
+ fontWeight: '600',
83
+ textAlign: 'center',
84
+ }}
85
+ >
86
+ Unlicensed
87
+ </button>
88
+ </div>
89
+ )
90
+ })
91
+
92
+ const WatermarkInner = memo(function WatermarkInner({
93
+ src,
94
+ isUnlicensed,
95
+ }: {
96
+ src: string
97
+ isUnlicensed: boolean
98
+ }) {
37
99
  const editor = useEditor()
38
100
  const isDebugMode = useValue('debug mode', () => editor.getInstanceState().isDebugMode, [editor])
39
101
  const isMobile = useValue('is mobile', () => editor.getViewportScreenBounds().width < 700, [
@@ -47,12 +109,17 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
47
109
  const maskCss = `url('${src}') center 100% / 100% no-repeat`
48
110
  const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
49
111
 
112
+ if (isUnlicensed) {
113
+ return <UnlicensedWatermark isDebugMode={isDebugMode} isMobile={isMobile} />
114
+ }
115
+
50
116
  return (
51
117
  <div
52
118
  ref={ref}
53
119
  className={LicenseManager.className}
54
120
  data-debug={isDebugMode}
55
121
  data-mobile={isMobile}
122
+ data-testid="tl-watermark-licensed"
56
123
  draggable={false}
57
124
  {...events}
58
125
  >
@@ -86,8 +153,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
86
153
 
87
154
  .${className} {
88
155
  position: absolute;
89
- bottom: var(--tl-space-2);
90
- right: var(--tl-space-2);
156
+ bottom: max(var(--tl-space-2), env(safe-area-inset-bottom));
157
+ right: max(var(--tl-space-2), env(safe-area-inset-right));
91
158
  width: 96px;
92
159
  height: 32px;
93
160
  display: flex;
@@ -116,12 +183,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
116
183
  }
117
184
 
118
185
  .${className}[data-debug='true'] {
119
- bottom: 46px;
186
+ bottom: max(46px, env(safe-area-inset-bottom));
120
187
  }
121
188
 
122
189
  .${className}[data-mobile='true'] {
123
190
  border-radius: 4px 0px 0px 4px;
124
- right: -2px;
191
+ right: max(-2px, calc(env(safe-area-inset-right) - 2px));
125
192
  width: 8px;
126
193
  height: 48px;
127
194
  }
@@ -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)
@@ -9,6 +9,8 @@ import {
9
9
  intersectLineSegmentPolyline,
10
10
  intersectPolys,
11
11
  linesIntersect,
12
+ polygonIntersectsPolyline,
13
+ polygonsIntersect,
12
14
  } from '../intersect'
13
15
  import { approximately, pointInPolygon } from '../utils'
14
16
 
@@ -227,25 +229,6 @@ export abstract class Geometry2d {
227
229
  return distanceAlongRoute / length
228
230
  }
229
231
 
230
- /** @deprecated Iterate the vertices instead. */
231
- nearestPointOnLineSegment(A: VecLike, B: VecLike): Vec {
232
- const { vertices } = this
233
- let nearest: Vec | undefined
234
- let dist = Infinity
235
- let d: number, p: Vec, q: Vec
236
- for (let i = 0; i < vertices.length; i++) {
237
- p = vertices[i]
238
- q = Vec.NearestPointOnLineSegment(A, B, p, true)
239
- d = Vec.Dist2(p, q)
240
- if (d < dist) {
241
- dist = d
242
- nearest = q
243
- }
244
- }
245
- if (!nearest) throw Error('nearest point not found')
246
- return nearest
247
- }
248
-
249
232
  isPointInBounds(point: VecLike, margin = 0) {
250
233
  const { bounds } = this
251
234
  return !(
@@ -256,6 +239,53 @@ export abstract class Geometry2d {
256
239
  )
257
240
  }
258
241
 
242
+ overlapsPolygon(_polygon: VecLike[]): boolean {
243
+ const polygon = _polygon.map((v) => Vec.From(v))
244
+
245
+ // Otherwise, check if the geometry itself overlaps the polygon
246
+ const { vertices, center, isFilled, isEmptyLabel, isClosed } = this
247
+
248
+ // We'll do things in order of cheapest to most expensive checks
249
+
250
+ // Skip empty labels
251
+ if (isEmptyLabel) return false
252
+
253
+ // If any of the geometry's vertices are inside the polygon, it's inside
254
+ if (vertices.some((v) => pointInPolygon(v, polygon))) {
255
+ return true
256
+ }
257
+
258
+ // If the geometry is filled and closed and its center is inside the polygon, it's inside
259
+ if (isClosed) {
260
+ if (isFilled) {
261
+ // If closed and filled, check if the center is inside the polygon
262
+ if (pointInPolygon(center, polygon)) {
263
+ return true
264
+ }
265
+
266
+ // ..then, slightly more expensive check, see the geometry covers the entire polygon but not its center
267
+ if (polygon.every((v) => pointInPolygon(v, vertices))) {
268
+ return true
269
+ }
270
+ }
271
+
272
+ // If any the geometry's vertices intersect the edge of the polygon, it's inside.
273
+ // for example when a rotated rectangle is moved over the corner of a parent rectangle
274
+ // If the geometry is closed, intersect as a polygon
275
+ if (polygonsIntersect(polygon, vertices)) {
276
+ return true
277
+ }
278
+ } else {
279
+ // If the geometry is not closed, intersect as a polyline
280
+ if (polygonIntersectsPolyline(polygon, vertices)) {
281
+ return true
282
+ }
283
+ }
284
+
285
+ // If none of the above checks passed, the geometry is outside the polygon
286
+ return false
287
+ }
288
+
259
289
  transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
260
290
  return new TransformedGeometry2d(this, transform, opts)
261
291
  }
@@ -236,4 +236,8 @@ export class Group2d extends Geometry2d {
236
236
  getSvgPathData(): string {
237
237
  return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
238
238
  }
239
+
240
+ overlapsPolygon(polygon: VecLike[]): boolean {
241
+ return this.children.some((child) => child.overlapsPolygon(polygon))
242
+ }
239
243
  }