@tldraw/editor 3.16.0-canary.5f82fb812214 → 3.16.0-canary.5f8d98bccb38
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.
- package/dist-cjs/index.d.ts +13 -101
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +1 -5
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +3 -99
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +110 -35
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +36 -3
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +68 -6
- package/dist-cjs/lib/license/Watermark.js.map +3 -3
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js +2 -35
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +13 -101
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +1 -5
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +3 -99
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +111 -36
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +36 -4
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +68 -6
- package/dist-esm/lib/license/Watermark.mjs.map +3 -3
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs +3 -40
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +8 -3
- package/package.json +7 -7
- package/src/index.ts +1 -9
- package/src/lib/TldrawEditor.tsx +1 -13
- package/src/lib/editor/Editor.ts +1 -125
- package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +11 -0
- package/src/lib/editor/types/misc-types.ts +0 -6
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/license/LicenseManager.test.ts +643 -387
- package/src/lib/license/LicenseManager.ts +156 -44
- package/src/lib/license/LicenseProvider.tsx +69 -5
- package/src/lib/license/Watermark.tsx +73 -6
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
- package/src/lib/primitives/geometry/Group2d.ts +4 -0
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/version.ts +3 -3
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { atom } from '@tldraw/state'
|
|
2
|
-
import {
|
|
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 =
|
|
6
|
+
const GRACE_PERIOD_DAYS = 30
|
|
8
7
|
|
|
9
8
|
export const FLAGS = {
|
|
10
|
-
ANNUAL_LICENSE:
|
|
11
|
-
PERPETUAL_LICENSE:
|
|
12
|
-
INTERNAL_LICENSE:
|
|
13
|
-
WITH_WATERMARK:
|
|
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
|
-
| '
|
|
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(
|
|
106
|
+
const licenseState = getLicenseState(
|
|
107
|
+
result,
|
|
108
|
+
(messages: string[]) => this.outputMessages(messages),
|
|
109
|
+
this.isDevelopment
|
|
110
|
+
)
|
|
100
111
|
|
|
101
|
-
|
|
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
|
-
|
|
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(
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -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
|
}
|