@tldraw/editor 3.16.0-canary.e1b1e53d3c16 → 3.16.0-canary.eb9a487b3293
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 +36 -101
- package/dist-cjs/index.js +1 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +0 -4
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +14 -109
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -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/exports/getSvgJsx.js +34 -14
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +7 -5
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- 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 +17 -22
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +5 -0
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +4 -4
- package/dist-cjs/lib/license/Watermark.js.map +1 -1
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- 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 +36 -101
- package/dist-esm/index.mjs +1 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +0 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +14 -109
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +34 -14
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +7 -5
- package/dist-esm/lib/hooks/useCanvasEvents.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 +17 -22
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +4 -4
- package/dist-esm/lib/license/Watermark.mjs.map +1 -1
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- 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 -0
- package/package.json +7 -7
- package/src/index.ts +1 -9
- package/src/lib/TldrawEditor.tsx +0 -11
- package/src/lib/components/default-components/DefaultCanvas.tsx +3 -1
- package/src/lib/editor/Editor.ts +20 -146
- package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
- package/src/lib/editor/types/misc-types.ts +0 -6
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +76 -19
- package/src/lib/hooks/useCanvasEvents.ts +6 -6
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/license/LicenseManager.test.ts +58 -51
- package/src/lib/license/LicenseManager.ts +32 -24
- package/src/lib/license/LicenseProvider.tsx +8 -0
- package/src/lib/license/Watermark.tsx +4 -4
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- 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
|
@@ -57,33 +57,21 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
57
57
|
.filter(({ id }) => shapeIdsToInclude.has(id))
|
|
58
58
|
|
|
59
59
|
// --- Common bounding box of all shapes
|
|
60
|
+
const singleFrameShapeId =
|
|
61
|
+
ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
|
|
62
|
+
? ids[0]
|
|
63
|
+
: null
|
|
64
|
+
|
|
60
65
|
let bbox: null | Box = null
|
|
61
66
|
if (opts.bounds) {
|
|
62
|
-
bbox = opts.bounds
|
|
67
|
+
bbox = opts.bounds.clone().expandBy(padding)
|
|
63
68
|
} else {
|
|
64
|
-
|
|
65
|
-
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
|
66
|
-
if (!maskedPageBounds) continue
|
|
67
|
-
if (bbox) {
|
|
68
|
-
bbox.union(maskedPageBounds)
|
|
69
|
-
} else {
|
|
70
|
-
bbox = maskedPageBounds.clone()
|
|
71
|
-
}
|
|
72
|
-
}
|
|
69
|
+
bbox = getExportDefaultBounds(editor, renderingShapes, padding, singleFrameShapeId)
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
// no unmasked shapes to export
|
|
76
73
|
if (!bbox) return
|
|
77
74
|
|
|
78
|
-
const singleFrameShapeId =
|
|
79
|
-
ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
|
|
80
|
-
? ids[0]
|
|
81
|
-
: null
|
|
82
|
-
if (!singleFrameShapeId) {
|
|
83
|
-
// Expand by an extra 32 pixels
|
|
84
|
-
bbox.expandBy(padding)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
75
|
// We want the svg image to be BIGGER THAN USUAL to account for image quality
|
|
88
76
|
const w = bbox.width * scale
|
|
89
77
|
const h = bbox.height * scale
|
|
@@ -120,6 +108,75 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
120
108
|
return { jsx: svg, width: w, height: h, exportDelay }
|
|
121
109
|
}
|
|
122
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Calculates the default bounds for an SVG export. This function handles:
|
|
113
|
+
* 1. Computing masked page bounds for each shape
|
|
114
|
+
* 2. Container logic: if a shape is marked as an export bounds container and it
|
|
115
|
+
* contains all other shapes, use its bounds and skip padding
|
|
116
|
+
* 3. Otherwise, create a union of all shape bounds and apply padding
|
|
117
|
+
*
|
|
118
|
+
* The container logic is useful for cases like annotating on an image - if the image
|
|
119
|
+
* contains all annotations, we want to export exactly the image bounds without extra padding.
|
|
120
|
+
*
|
|
121
|
+
* @param editor - The editor instance
|
|
122
|
+
* @param renderingShapes - The shapes to include in the export
|
|
123
|
+
* @param padding - Padding to add around the bounds (only applied if no container bounds)
|
|
124
|
+
* @param singleFrameShapeId - If exporting a single frame, this is its ID (skips padding)
|
|
125
|
+
* @returns The calculated bounds box, or null if no shapes to export
|
|
126
|
+
*/
|
|
127
|
+
export function getExportDefaultBounds(
|
|
128
|
+
editor: Editor,
|
|
129
|
+
renderingShapes: TLRenderingShape[],
|
|
130
|
+
padding: number,
|
|
131
|
+
singleFrameShapeId: TLShapeId | null
|
|
132
|
+
) {
|
|
133
|
+
let isBoundedByContainer = false
|
|
134
|
+
let bbox: null | Box = null
|
|
135
|
+
|
|
136
|
+
for (const { id } of renderingShapes) {
|
|
137
|
+
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
|
138
|
+
if (!maskedPageBounds) continue
|
|
139
|
+
|
|
140
|
+
// Check if this shape is an export bounds container (e.g., an image being annotated)
|
|
141
|
+
const shape = editor.getShape(id)!
|
|
142
|
+
const isContainer = editor.getShapeUtil(shape).isExportBoundsContainer(shape)
|
|
143
|
+
|
|
144
|
+
if (bbox) {
|
|
145
|
+
// Container logic: if this is a container and it contains all shapes processed so far,
|
|
146
|
+
// use the container's bounds instead of the union. This prevents extra padding around
|
|
147
|
+
// things like annotated images.
|
|
148
|
+
if (isContainer && Box.ContainsApproximately(maskedPageBounds, bbox)) {
|
|
149
|
+
isBoundedByContainer = true
|
|
150
|
+
bbox = maskedPageBounds.clone()
|
|
151
|
+
} else {
|
|
152
|
+
// If we were previously bounded by a container but this shape extends outside it,
|
|
153
|
+
// we're no longer bounded by a container
|
|
154
|
+
if (isBoundedByContainer && !Box.ContainsApproximately(bbox, maskedPageBounds)) {
|
|
155
|
+
isBoundedByContainer = false
|
|
156
|
+
}
|
|
157
|
+
// Expand the bounding box to include this shape
|
|
158
|
+
bbox.union(maskedPageBounds)
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// First shape sets the initial bounds
|
|
162
|
+
isBoundedByContainer = isContainer
|
|
163
|
+
bbox = maskedPageBounds.clone()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// No unmasked shapes to export
|
|
168
|
+
if (!bbox) return null
|
|
169
|
+
|
|
170
|
+
// Only apply padding if:
|
|
171
|
+
// - Not exporting a single frame (frames have their own padding rules)
|
|
172
|
+
// - Not bounded by a container (containers define their own bounds precisely)
|
|
173
|
+
if (!singleFrameShapeId && !isBoundedByContainer) {
|
|
174
|
+
bbox.expandBy(padding)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return bbox
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
function SvgExport({
|
|
124
181
|
editor,
|
|
125
182
|
preserveAspectRatio,
|
|
@@ -79,15 +79,15 @@ export function useCanvasEvents() {
|
|
|
79
79
|
// check that e.target is an HTMLElement
|
|
80
80
|
if (!(e.target instanceof HTMLElement)) return
|
|
81
81
|
|
|
82
|
+
const editingShapeId = editor.getEditingShape()?.id
|
|
82
83
|
if (
|
|
84
|
+
// if the target is not inside the editing shape
|
|
85
|
+
!(editingShapeId && e.target.closest(`[data-shape-id="${editingShapeId}"]`)) &&
|
|
86
|
+
// and the target is not an clickable element
|
|
83
87
|
e.target.tagName !== 'A' &&
|
|
88
|
+
// or a TextArea.tsx ?
|
|
84
89
|
e.target.tagName !== 'TEXTAREA' &&
|
|
85
|
-
!e.target.isContentEditable
|
|
86
|
-
// When in EditingShape state, we are actually clicking on a 'DIV'
|
|
87
|
-
// not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
|
|
88
|
-
// for edit mode on mobile we need to not preventDefault.
|
|
89
|
-
// TODO: Find out if we still need this preventDefault in general though.
|
|
90
|
-
!(editor.getEditingShape() && e.target.className.includes('tl-text-content'))
|
|
90
|
+
!e.target.isContentEditable
|
|
91
91
|
) {
|
|
92
92
|
preventDefault(e)
|
|
93
93
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { RefObject, useEffect } from 'react'
|
|
2
2
|
import { preventDefault } from '../utils/dom'
|
|
3
3
|
import { useContainer } from './useContainer'
|
|
4
|
+
import { useMaybeEditor } from './useEditor'
|
|
4
5
|
|
|
5
6
|
/** @public */
|
|
6
7
|
export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
|
|
7
8
|
if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
|
|
8
9
|
const container = useContainer()
|
|
10
|
+
const editor = useMaybeEditor()
|
|
9
11
|
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
function onMouseOver(e: MouseEvent) {
|
|
14
|
+
if (!editor?.getInstanceState().isFocused) return
|
|
12
15
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
13
16
|
preventDefault(e)
|
|
14
17
|
const cvs = container.querySelector('.tl-canvas')
|
|
@@ -25,5 +28,5 @@ export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
|
|
|
25
28
|
return () => {
|
|
26
29
|
elm.removeEventListener('mouseover', onMouseOver)
|
|
27
30
|
}
|
|
28
|
-
}, [container, ref])
|
|
31
|
+
}, [container, editor, ref])
|
|
29
32
|
}
|
|
@@ -4,7 +4,7 @@ import { publishDates } from '../../version'
|
|
|
4
4
|
import { str2ab } from '../utils/licensing'
|
|
5
5
|
import {
|
|
6
6
|
FLAGS,
|
|
7
|
-
|
|
7
|
+
getLicenseState,
|
|
8
8
|
LicenseManager,
|
|
9
9
|
PROPERTIES,
|
|
10
10
|
ValidLicenseKeyResult,
|
|
@@ -487,115 +487,122 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
describe(
|
|
491
|
-
it('
|
|
490
|
+
describe('getLicenseState', () => {
|
|
491
|
+
it('returns "unlicensed" for unparseable license', () => {
|
|
492
492
|
const licenseResult = getDefaultLicenseResult({
|
|
493
493
|
// @ts-ignore
|
|
494
494
|
isLicenseParseable: false,
|
|
495
495
|
})
|
|
496
|
-
expect(
|
|
496
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
497
497
|
})
|
|
498
498
|
|
|
499
|
-
it('
|
|
499
|
+
it('returns "unlicensed" for invalid domain in production', () => {
|
|
500
500
|
const licenseResult = getDefaultLicenseResult({
|
|
501
501
|
isDomainValid: false,
|
|
502
|
+
isDevelopment: false,
|
|
502
503
|
})
|
|
503
|
-
expect(
|
|
504
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
504
505
|
})
|
|
505
506
|
|
|
506
|
-
it('
|
|
507
|
+
it('returns "licensed" for invalid domain in development mode', () => {
|
|
508
|
+
const licenseResult = getDefaultLicenseResult({
|
|
509
|
+
isDomainValid: false,
|
|
510
|
+
isDevelopment: true,
|
|
511
|
+
})
|
|
512
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('returns "unlicensed" for expired annual license', () => {
|
|
507
516
|
const licenseResult = getDefaultLicenseResult({
|
|
508
517
|
isAnnualLicense: true,
|
|
509
518
|
isAnnualLicenseExpired: true,
|
|
519
|
+
isInternalLicense: false,
|
|
510
520
|
})
|
|
511
|
-
expect(
|
|
521
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
512
522
|
})
|
|
513
523
|
|
|
514
|
-
it('
|
|
524
|
+
it('returns "unlicensed" for expired annual license even in dev mode', () => {
|
|
515
525
|
const licenseResult = getDefaultLicenseResult({
|
|
516
526
|
isAnnualLicense: true,
|
|
517
527
|
isAnnualLicenseExpired: true,
|
|
518
528
|
isDevelopment: true,
|
|
529
|
+
isInternalLicense: false,
|
|
519
530
|
})
|
|
520
|
-
expect(
|
|
531
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
521
532
|
})
|
|
522
533
|
|
|
523
|
-
it('
|
|
534
|
+
it('returns "unlicensed" for expired perpetual license', () => {
|
|
524
535
|
const licenseResult = getDefaultLicenseResult({
|
|
525
536
|
isPerpetualLicense: true,
|
|
526
537
|
isPerpetualLicenseExpired: true,
|
|
538
|
+
isInternalLicense: false,
|
|
527
539
|
})
|
|
528
|
-
expect(
|
|
540
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
529
541
|
})
|
|
530
542
|
|
|
531
|
-
it('
|
|
543
|
+
it('returns "internal-expired" for expired internal annual license with valid domain', () => {
|
|
544
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
532
545
|
const licenseResult = getDefaultLicenseResult({
|
|
533
546
|
isAnnualLicense: true,
|
|
534
|
-
isAnnualLicenseExpired:
|
|
535
|
-
isInternalLicense:
|
|
547
|
+
isAnnualLicenseExpired: true,
|
|
548
|
+
isInternalLicense: true,
|
|
549
|
+
isDomainValid: true,
|
|
550
|
+
expiryDate,
|
|
536
551
|
})
|
|
537
|
-
expect(
|
|
552
|
+
expect(getLicenseState(licenseResult)).toBe('internal-expired')
|
|
538
553
|
})
|
|
539
554
|
|
|
540
|
-
it('
|
|
555
|
+
it('returns "internal-expired" for expired internal perpetual license with valid domain', () => {
|
|
556
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
541
557
|
const licenseResult = getDefaultLicenseResult({
|
|
542
558
|
isPerpetualLicense: true,
|
|
543
|
-
isPerpetualLicenseExpired:
|
|
544
|
-
isInternalLicense:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
it('does not show watermark when in development mode', () => {
|
|
550
|
-
const licenseResult = getDefaultLicenseResult({
|
|
551
|
-
isDevelopment: true,
|
|
559
|
+
isPerpetualLicenseExpired: true,
|
|
560
|
+
isInternalLicense: true,
|
|
561
|
+
isDomainValid: true,
|
|
562
|
+
expiryDate,
|
|
552
563
|
})
|
|
553
|
-
expect(
|
|
564
|
+
expect(getLicenseState(licenseResult)).toBe('internal-expired')
|
|
554
565
|
})
|
|
555
566
|
|
|
556
|
-
it('
|
|
567
|
+
it('returns "unlicensed" for expired internal license with invalid domain', () => {
|
|
568
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
557
569
|
const licenseResult = getDefaultLicenseResult({
|
|
558
|
-
|
|
559
|
-
|
|
570
|
+
isAnnualLicense: true,
|
|
571
|
+
isAnnualLicenseExpired: true,
|
|
572
|
+
isInternalLicense: true,
|
|
573
|
+
isDomainValid: false,
|
|
574
|
+
expiryDate,
|
|
560
575
|
})
|
|
561
|
-
expect(
|
|
576
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
562
577
|
})
|
|
563
578
|
|
|
564
|
-
it('
|
|
579
|
+
it('returns "licensed-with-watermark" for watermarked license', () => {
|
|
565
580
|
const licenseResult = getDefaultLicenseResult({
|
|
566
|
-
|
|
567
|
-
isDomainValid: false,
|
|
568
|
-
isDevelopment: true,
|
|
581
|
+
isLicensedWithWatermark: true,
|
|
569
582
|
})
|
|
570
|
-
expect(
|
|
583
|
+
expect(getLicenseState(licenseResult)).toBe('licensed-with-watermark')
|
|
571
584
|
})
|
|
572
585
|
|
|
573
|
-
it('
|
|
574
|
-
const expiryDate = new Date(2023, 1, 1)
|
|
586
|
+
it('returns "licensed" for valid annual license', () => {
|
|
575
587
|
const licenseResult = getDefaultLicenseResult({
|
|
576
588
|
isAnnualLicense: true,
|
|
577
|
-
isAnnualLicenseExpired:
|
|
578
|
-
isInternalLicense: true,
|
|
579
|
-
expiryDate,
|
|
589
|
+
isAnnualLicenseExpired: false,
|
|
580
590
|
})
|
|
581
|
-
expect((
|
|
591
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
582
592
|
})
|
|
583
593
|
|
|
584
|
-
it('
|
|
585
|
-
const expiryDate = new Date(2023, 1, 1)
|
|
594
|
+
it('returns "licensed" for valid perpetual license', () => {
|
|
586
595
|
const licenseResult = getDefaultLicenseResult({
|
|
587
596
|
isPerpetualLicense: true,
|
|
588
|
-
isPerpetualLicenseExpired:
|
|
589
|
-
isInternalLicense: true,
|
|
590
|
-
expiryDate,
|
|
597
|
+
isPerpetualLicenseExpired: false,
|
|
591
598
|
})
|
|
592
|
-
expect((
|
|
599
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
593
600
|
})
|
|
594
601
|
|
|
595
|
-
it('
|
|
602
|
+
it('returns "licensed" for valid license in development mode', () => {
|
|
596
603
|
const licenseResult = getDefaultLicenseResult({
|
|
597
|
-
|
|
604
|
+
isDevelopment: true,
|
|
598
605
|
})
|
|
599
|
-
expect(
|
|
606
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
600
607
|
})
|
|
601
608
|
})
|
|
@@ -33,6 +33,14 @@ export interface LicenseInfo {
|
|
|
33
33
|
flags: number
|
|
34
34
|
expiryDate: string
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export type LicenseState =
|
|
39
|
+
| 'pending'
|
|
40
|
+
| 'licensed'
|
|
41
|
+
| 'licensed-with-watermark'
|
|
42
|
+
| 'unlicensed'
|
|
43
|
+
| 'internal-expired'
|
|
36
44
|
/** @internal */
|
|
37
45
|
export type InvalidLicenseReason =
|
|
38
46
|
| 'invalid-license-key'
|
|
@@ -73,10 +81,7 @@ export class LicenseManager {
|
|
|
73
81
|
public isDevelopment: boolean
|
|
74
82
|
public isTest: boolean
|
|
75
83
|
public isCryptoAvailable: boolean
|
|
76
|
-
state = atom<'
|
|
77
|
-
'license state',
|
|
78
|
-
'pending'
|
|
79
|
-
)
|
|
84
|
+
state = atom<LicenseState>('license state', 'pending')
|
|
80
85
|
public verbose = true
|
|
81
86
|
|
|
82
87
|
constructor(
|
|
@@ -89,21 +94,20 @@ export class LicenseManager {
|
|
|
89
94
|
this.publicKey = testPublicKey || this.publicKey
|
|
90
95
|
this.isCryptoAvailable = !!crypto.subtle
|
|
91
96
|
|
|
92
|
-
this.getLicenseFromKey(licenseKey)
|
|
93
|
-
|
|
97
|
+
this.getLicenseFromKey(licenseKey)
|
|
98
|
+
.then((result) => {
|
|
99
|
+
const licenseState = getLicenseState(result)
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
if (!this.isDevelopment && licenseState === 'unlicensed') {
|
|
102
|
+
fetch(WATERMARK_TRACK_SRC)
|
|
103
|
+
}
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
this.state.set(licenseState)
|
|
106
|
+
})
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
console.error('License validation failed:', error)
|
|
100
109
|
this.state.set('unlicensed')
|
|
101
|
-
}
|
|
102
|
-
this.state.set('licensed-with-watermark')
|
|
103
|
-
} else {
|
|
104
|
-
this.state.set('licensed')
|
|
105
|
-
}
|
|
106
|
-
})
|
|
110
|
+
})
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
private getIsDevelopment(testEnvironment?: TestEnvironment) {
|
|
@@ -367,15 +371,19 @@ export class LicenseManager {
|
|
|
367
371
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
368
372
|
}
|
|
369
373
|
|
|
370
|
-
export function
|
|
371
|
-
if (!result.isLicenseParseable) return
|
|
372
|
-
if (!result.isDomainValid && !result.isDevelopment) return
|
|
374
|
+
export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
|
|
375
|
+
if (!result.isLicenseParseable) return 'unlicensed'
|
|
376
|
+
if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
|
|
373
377
|
if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
|
|
374
|
-
if
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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'
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// License is valid, determine if it has watermark
|
|
384
|
+
if (result.isLicensedWithWatermark) {
|
|
385
|
+
return 'licensed-with-watermark'
|
|
378
386
|
}
|
|
379
387
|
|
|
380
|
-
return
|
|
388
|
+
return 'licensed'
|
|
381
389
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useValue } from '@tldraw/state-react'
|
|
1
2
|
import { createContext, ReactNode, useContext, useState } from 'react'
|
|
2
3
|
import { LicenseManager } from './LicenseManager'
|
|
3
4
|
|
|
@@ -16,5 +17,12 @@ export function LicenseProvider({
|
|
|
16
17
|
children: ReactNode
|
|
17
18
|
}) {
|
|
18
19
|
const [licenseManager] = useState(() => new LicenseManager(licenseKey))
|
|
20
|
+
const licenseState = useValue(licenseManager.state)
|
|
21
|
+
|
|
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' }} />
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
|
|
20
28
|
}
|
|
@@ -86,8 +86,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
86
86
|
|
|
87
87
|
.${className} {
|
|
88
88
|
position: absolute;
|
|
89
|
-
bottom: var(--tl-space-2);
|
|
90
|
-
right: var(--tl-space-2);
|
|
89
|
+
bottom: max(var(--tl-space-2), env(safe-area-inset-bottom));
|
|
90
|
+
right: max(var(--tl-space-2), env(safe-area-inset-right));
|
|
91
91
|
width: 96px;
|
|
92
92
|
height: 32px;
|
|
93
93
|
display: flex;
|
|
@@ -116,12 +116,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.${className}[data-debug='true'] {
|
|
119
|
-
bottom: 46px;
|
|
119
|
+
bottom: max(46px, env(safe-area-inset-bottom));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
.${className}[data-mobile='true'] {
|
|
123
123
|
border-radius: 4px 0px 0px 4px;
|
|
124
|
-
right: -2px;
|
|
124
|
+
right: max(-2px, calc(env(safe-area-inset-right) - 2px));
|
|
125
125
|
width: 8px;
|
|
126
126
|
height: 48px;
|
|
127
127
|
}
|
|
@@ -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
|
}
|