@tldraw/editor 3.16.0-canary.2d1a57ea21b2 → 3.16.0-canary.2e83e38fb91b
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 +49 -3
- package/dist-cjs/index.js +4 -2
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +1 -1
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +11 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +9 -4
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +15 -12
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useHandleEvents.js +3 -3
- package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useSelectionEvents.js +4 -4
- package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +21 -4
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +2 -2
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +24 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +5 -1
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +12 -1
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +49 -3
- package/dist-esm/index.mjs +7 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +2 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +12 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +9 -4
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -13
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +11 -6
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useHandleEvents.mjs +9 -4
- package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useSelectionEvents.mjs +6 -5
- package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +21 -4
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +3 -3
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +24 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +5 -1
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +12 -1
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +2 -0
- package/src/lib/TldrawEditor.tsx +2 -2
- package/src/lib/components/default-components/DefaultCanvas.tsx +8 -2
- package/src/lib/editor/Editor.test.ts +90 -0
- package/src/lib/editor/Editor.ts +16 -4
- package/src/lib/hooks/useCanvasEvents.ts +17 -11
- package/src/lib/hooks/useDocumentEvents.ts +11 -6
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -2
- package/src/lib/hooks/useGestureEvents.ts +2 -2
- package/src/lib/hooks/useHandleEvents.ts +9 -4
- package/src/lib/hooks/useSelectionEvents.ts +6 -5
- package/src/lib/license/LicenseManager.test.ts +78 -2
- package/src/lib/license/LicenseManager.ts +28 -5
- package/src/lib/license/Watermark.tsx +3 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +29 -2
- package/src/lib/primitives/geometry/Group2d.ts +6 -1
- package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
- package/src/lib/utils/dom.test.ts +94 -0
- package/src/lib/utils/dom.ts +38 -1
- package/src/lib/utils/getPointerInfo.ts +2 -1
- package/src/version.ts +3 -3
|
@@ -266,7 +266,7 @@ describe('LicenseManager', () => {
|
|
|
266
266
|
delete window.location
|
|
267
267
|
// @ts-ignore
|
|
268
268
|
window.location = new URL(
|
|
269
|
-
'vscode-webview
|
|
269
|
+
'vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
|
|
270
270
|
)
|
|
271
271
|
|
|
272
272
|
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
@@ -286,7 +286,7 @@ describe('LicenseManager', () => {
|
|
|
286
286
|
delete window.location
|
|
287
287
|
// @ts-ignore
|
|
288
288
|
window.location = new URL(
|
|
289
|
-
'vscode-webview
|
|
289
|
+
'vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
|
|
290
290
|
)
|
|
291
291
|
|
|
292
292
|
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
@@ -300,6 +300,70 @@ describe('LicenseManager', () => {
|
|
|
300
300
|
)) as ValidLicenseKeyResult
|
|
301
301
|
expect(result.isDomainValid).toBe(false)
|
|
302
302
|
})
|
|
303
|
+
|
|
304
|
+
it('Succeeds if it is a native app', async () => {
|
|
305
|
+
// @ts-ignore
|
|
306
|
+
delete window.location
|
|
307
|
+
// @ts-ignore
|
|
308
|
+
window.location = new URL('app-bundle://app/index.html')
|
|
309
|
+
|
|
310
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
311
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
312
|
+
nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
|
|
313
|
+
const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
|
|
314
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
315
|
+
nativeLicenseKey
|
|
316
|
+
)) as ValidLicenseKeyResult
|
|
317
|
+
expect(result.isDomainValid).toBe(true)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('Succeeds if it is a native app with a wildcard', async () => {
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
delete window.location
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
window.location = new URL('app-bundle://unique-id-123/index.html')
|
|
325
|
+
|
|
326
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
327
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
328
|
+
nativeLicenseInfo[PROPERTIES.HOSTS] = ['^app-bundle://unique-id-123.*']
|
|
329
|
+
const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
|
|
330
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
331
|
+
nativeLicenseKey
|
|
332
|
+
)) as ValidLicenseKeyResult
|
|
333
|
+
expect(result.isDomainValid).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('Succeeds if it is a native app with a wildcard and search param', async () => {
|
|
337
|
+
// @ts-ignore
|
|
338
|
+
delete window.location
|
|
339
|
+
// @ts-ignore
|
|
340
|
+
window.location = new URL('app-bundle://app/index.html?unique-id-123')
|
|
341
|
+
|
|
342
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
343
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
344
|
+
nativeLicenseInfo[PROPERTIES.HOSTS] = ['^app-bundle://app.*unique-id-123.*']
|
|
345
|
+
const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
|
|
346
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
347
|
+
nativeLicenseKey
|
|
348
|
+
)) as ValidLicenseKeyResult
|
|
349
|
+
expect(result.isDomainValid).toBe(true)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('Fails if it is a native app with the wrong protocol', async () => {
|
|
353
|
+
// @ts-ignore
|
|
354
|
+
delete window.location
|
|
355
|
+
// @ts-ignore
|
|
356
|
+
window.location = new URL('blah-blundle://app/index.html')
|
|
357
|
+
|
|
358
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
359
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
360
|
+
nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
|
|
361
|
+
const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
|
|
362
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
363
|
+
nativeLicenseKey
|
|
364
|
+
)) as ValidLicenseKeyResult
|
|
365
|
+
expect(result.isDomainValid).toBe(false)
|
|
366
|
+
})
|
|
303
367
|
})
|
|
304
368
|
|
|
305
369
|
describe('License types and flags', () => {
|
|
@@ -316,6 +380,17 @@ describe('LicenseManager', () => {
|
|
|
316
380
|
expect(result.isInternalLicense).toBe(true)
|
|
317
381
|
})
|
|
318
382
|
|
|
383
|
+
it('Checks for native license', async () => {
|
|
384
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
385
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
386
|
+
const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
|
|
387
|
+
|
|
388
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
389
|
+
nativeLicenseKey
|
|
390
|
+
)) as ValidLicenseKeyResult
|
|
391
|
+
expect(result.isNativeLicense).toBe(true)
|
|
392
|
+
})
|
|
393
|
+
|
|
319
394
|
it('Checks for license with watermark', async () => {
|
|
320
395
|
const withWatermarkLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
321
396
|
withWatermarkLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.WITH_WATERMARK
|
|
@@ -553,6 +628,7 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
|
|
|
553
628
|
isAnnualLicense: true,
|
|
554
629
|
isAnnualLicenseExpired: false,
|
|
555
630
|
isInternalLicense: false,
|
|
631
|
+
isNativeLicense: false,
|
|
556
632
|
isDevelopment: false,
|
|
557
633
|
isDomainValid: true,
|
|
558
634
|
isPerpetualLicense: false,
|
|
@@ -6,11 +6,22 @@ import { importPublicKey, str2ab } from '../utils/licensing'
|
|
|
6
6
|
const GRACE_PERIOD_DAYS = 30
|
|
7
7
|
|
|
8
8
|
export const FLAGS = {
|
|
9
|
+
// -- MUTUALLY EXCLUSIVE FLAGS --
|
|
10
|
+
// Annual means the license expires after a time period, usually 1 year.
|
|
9
11
|
ANNUAL_LICENSE: 1,
|
|
12
|
+
// Perpetual means the license never expires up to the max supported version.
|
|
10
13
|
PERPETUAL_LICENSE: 1 << 1,
|
|
14
|
+
|
|
15
|
+
// -- ADDITIVE FLAGS --
|
|
16
|
+
// Internal means the license is for internal use only.
|
|
11
17
|
INTERNAL_LICENSE: 1 << 2,
|
|
18
|
+
// Watermark means the product is watermarked.
|
|
12
19
|
WITH_WATERMARK: 1 << 3,
|
|
20
|
+
// Evaluation means the license is for evaluation purposes only.
|
|
13
21
|
EVALUATION_LICENSE: 1 << 4,
|
|
22
|
+
// Native means the license is for native apps which switches
|
|
23
|
+
// on special-case logic.
|
|
24
|
+
NATIVE_LICENSE: 1 << 5,
|
|
14
25
|
}
|
|
15
26
|
const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
|
|
16
27
|
|
|
@@ -69,6 +80,7 @@ export interface ValidLicenseKeyResult {
|
|
|
69
80
|
isPerpetualLicense: boolean
|
|
70
81
|
isPerpetualLicenseExpired: boolean
|
|
71
82
|
isInternalLicense: boolean
|
|
83
|
+
isNativeLicense: boolean
|
|
72
84
|
isLicensedWithWatermark: boolean
|
|
73
85
|
isEvaluationLicense: boolean
|
|
74
86
|
isEvaluationLicenseExpired: boolean
|
|
@@ -271,6 +283,7 @@ export class LicenseManager {
|
|
|
271
283
|
isPerpetualLicense,
|
|
272
284
|
isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
|
|
273
285
|
isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
|
|
286
|
+
isNativeLicense: this.isNativeLicense(licenseInfo),
|
|
274
287
|
isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
|
|
275
288
|
isEvaluationLicense,
|
|
276
289
|
isEvaluationLicenseExpired:
|
|
@@ -291,13 +304,13 @@ export class LicenseManager {
|
|
|
291
304
|
const currentHostname = window.location.hostname.toLowerCase()
|
|
292
305
|
|
|
293
306
|
return licenseInfo.hosts.some((host) => {
|
|
294
|
-
const
|
|
307
|
+
const normalizedHostOrUrlRegex = host.toLowerCase().trim()
|
|
295
308
|
|
|
296
309
|
// Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
|
|
297
310
|
if (
|
|
298
|
-
|
|
299
|
-
`www.${
|
|
300
|
-
|
|
311
|
+
normalizedHostOrUrlRegex === currentHostname ||
|
|
312
|
+
`www.${normalizedHostOrUrlRegex}` === currentHostname ||
|
|
313
|
+
normalizedHostOrUrlRegex === `www.${currentHostname}`
|
|
301
314
|
) {
|
|
302
315
|
return true
|
|
303
316
|
}
|
|
@@ -308,6 +321,12 @@ export class LicenseManager {
|
|
|
308
321
|
return true
|
|
309
322
|
}
|
|
310
323
|
|
|
324
|
+
// Native license support
|
|
325
|
+
// In this case, `normalizedHost` is actually a protocol, e.g. `app-bundle:`
|
|
326
|
+
if (this.isNativeLicense(licenseInfo)) {
|
|
327
|
+
return new RegExp(normalizedHostOrUrlRegex).test(window.location.href)
|
|
328
|
+
}
|
|
329
|
+
|
|
311
330
|
// Glob testing, we only support '*.somedomain.com' right now.
|
|
312
331
|
if (host.includes('*')) {
|
|
313
332
|
const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
|
|
@@ -318,7 +337,7 @@ export class LicenseManager {
|
|
|
318
337
|
if (window.location.protocol === 'vscode-webview:') {
|
|
319
338
|
const currentUrl = new URL(window.location.href)
|
|
320
339
|
const extensionId = currentUrl.searchParams.get('extensionId')
|
|
321
|
-
if (
|
|
340
|
+
if (normalizedHostOrUrlRegex === extensionId) {
|
|
322
341
|
return true
|
|
323
342
|
}
|
|
324
343
|
}
|
|
@@ -327,6 +346,10 @@ export class LicenseManager {
|
|
|
327
346
|
})
|
|
328
347
|
}
|
|
329
348
|
|
|
349
|
+
private isNativeLicense(licenseInfo: LicenseInfo) {
|
|
350
|
+
return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
|
|
351
|
+
}
|
|
352
|
+
|
|
330
353
|
private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
|
|
331
354
|
return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
|
|
332
355
|
}
|
|
@@ -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 {
|
|
6
|
+
import { markEventAsHandled, preventDefault } from '../utils/dom'
|
|
7
7
|
import { runtime } from '../utils/runtime'
|
|
8
8
|
import { watermarkDesktopSvg, watermarkMobileSvg } from '../watermarks'
|
|
9
9
|
import { LicenseManager } from './LicenseManager'
|
|
@@ -64,7 +64,7 @@ const UnlicensedWatermark = memo(function UnlicensedWatermark({
|
|
|
64
64
|
draggable={false}
|
|
65
65
|
role="button"
|
|
66
66
|
onPointerDown={(e) => {
|
|
67
|
-
|
|
67
|
+
markEventAsHandled(e)
|
|
68
68
|
preventDefault(e)
|
|
69
69
|
}}
|
|
70
70
|
title="Unlicensed - click to get a license"
|
|
@@ -127,7 +127,7 @@ const WatermarkInner = memo(function WatermarkInner({
|
|
|
127
127
|
draggable={false}
|
|
128
128
|
role="button"
|
|
129
129
|
onPointerDown={(e) => {
|
|
130
|
-
|
|
130
|
+
markEventAsHandled(e)
|
|
131
131
|
preventDefault(e)
|
|
132
132
|
}}
|
|
133
133
|
title="made with tldraw"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Mat } from '../Mat'
|
|
2
2
|
import { Vec, VecLike } from '../Vec'
|
|
3
3
|
import { Geometry2dFilters } from './Geometry2d'
|
|
4
|
+
import { Group2d } from './Group2d'
|
|
4
5
|
import { Rectangle2d } from './Rectangle2d'
|
|
5
6
|
|
|
6
7
|
describe('TransformedGeometry2d', () => {
|
|
@@ -36,6 +37,425 @@ describe('TransformedGeometry2d', () => {
|
|
|
36
37
|
})
|
|
37
38
|
})
|
|
38
39
|
|
|
40
|
+
describe('excludeFromShapeBounds', () => {
|
|
41
|
+
test('simple geometry with excludeFromShapeBounds flag', () => {
|
|
42
|
+
const rect = new Rectangle2d({
|
|
43
|
+
width: 100,
|
|
44
|
+
height: 50,
|
|
45
|
+
isFilled: true,
|
|
46
|
+
excludeFromShapeBounds: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// The bounds should still be calculated normally for simple geometry
|
|
50
|
+
const bounds = rect.bounds
|
|
51
|
+
expect(bounds.width).toBe(100)
|
|
52
|
+
expect(bounds.height).toBe(50)
|
|
53
|
+
expect(bounds.x).toBe(0)
|
|
54
|
+
expect(bounds.y).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('group with excluded child geometry', () => {
|
|
58
|
+
const mainRect = new Rectangle2d({
|
|
59
|
+
width: 100,
|
|
60
|
+
height: 50,
|
|
61
|
+
isFilled: true,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const excludedRect = new Rectangle2d({
|
|
65
|
+
width: 200,
|
|
66
|
+
height: 100,
|
|
67
|
+
isFilled: true,
|
|
68
|
+
excludeFromShapeBounds: true,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const group = new Group2d({
|
|
72
|
+
children: [mainRect, excludedRect],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// The bounds should only include the non-excluded rectangle
|
|
76
|
+
const bounds = group.bounds
|
|
77
|
+
expect(bounds.width).toBe(100) // Only the main rectangle width
|
|
78
|
+
expect(bounds.height).toBe(50) // Only the main rectangle height
|
|
79
|
+
expect(bounds.x).toBe(0)
|
|
80
|
+
expect(bounds.y).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('group with multiple excluded children', () => {
|
|
84
|
+
const rect1 = new Rectangle2d({
|
|
85
|
+
width: 50,
|
|
86
|
+
height: 50,
|
|
87
|
+
isFilled: true,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const rect2 = new Rectangle2d({
|
|
91
|
+
width: 100,
|
|
92
|
+
height: 30,
|
|
93
|
+
isFilled: true,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const excludedRect1 = new Rectangle2d({
|
|
97
|
+
width: 200,
|
|
98
|
+
height: 200,
|
|
99
|
+
isFilled: true,
|
|
100
|
+
excludeFromShapeBounds: true,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const excludedRect2 = new Rectangle2d({
|
|
104
|
+
width: 300,
|
|
105
|
+
height: 300,
|
|
106
|
+
isFilled: true,
|
|
107
|
+
excludeFromShapeBounds: true,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const group = new Group2d({
|
|
111
|
+
children: [rect1, excludedRect1, rect2, excludedRect2],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// The bounds should include both non-excluded rectangles
|
|
115
|
+
const bounds = group.bounds
|
|
116
|
+
expect(bounds.width).toBe(100) // Width of rect2 (larger of the two)
|
|
117
|
+
expect(bounds.height).toBe(50) // Height of rect1 (larger of the two)
|
|
118
|
+
expect(bounds.x).toBe(0)
|
|
119
|
+
expect(bounds.y).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('group with all children excluded', () => {
|
|
123
|
+
const excludedRect1 = new Rectangle2d({
|
|
124
|
+
width: 100,
|
|
125
|
+
height: 50,
|
|
126
|
+
isFilled: true,
|
|
127
|
+
excludeFromShapeBounds: true,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const excludedRect2 = new Rectangle2d({
|
|
131
|
+
width: 200,
|
|
132
|
+
height: 100,
|
|
133
|
+
isFilled: true,
|
|
134
|
+
excludeFromShapeBounds: true,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const group = new Group2d({
|
|
138
|
+
children: [excludedRect1, excludedRect2],
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// The bounds should be empty when all children are excluded
|
|
142
|
+
const bounds = group.bounds
|
|
143
|
+
expect(bounds.width).toBe(0)
|
|
144
|
+
expect(bounds.height).toBe(0)
|
|
145
|
+
expect(bounds.x).toBe(0)
|
|
146
|
+
expect(bounds.y).toBe(0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('nested groups with excluded geometry', () => {
|
|
150
|
+
const innerRect = new Rectangle2d({
|
|
151
|
+
width: 50,
|
|
152
|
+
height: 50,
|
|
153
|
+
isFilled: true,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const excludedRect = new Rectangle2d({
|
|
157
|
+
width: 200,
|
|
158
|
+
height: 200,
|
|
159
|
+
isFilled: true,
|
|
160
|
+
excludeFromShapeBounds: true,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const innerGroup = new Group2d({
|
|
164
|
+
children: [innerRect, excludedRect],
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const outerRect = new Rectangle2d({
|
|
168
|
+
width: 100,
|
|
169
|
+
height: 30,
|
|
170
|
+
isFilled: true,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const outerGroup = new Group2d({
|
|
174
|
+
children: [innerGroup, outerRect],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// The bounds should include both the inner group (without excluded rect) and outer rect
|
|
178
|
+
const bounds = outerGroup.bounds
|
|
179
|
+
expect(bounds.width).toBe(100) // Width of outerRect (larger)
|
|
180
|
+
expect(bounds.height).toBe(50) // Height of innerRect (larger)
|
|
181
|
+
expect(bounds.x).toBe(0)
|
|
182
|
+
expect(bounds.y).toBe(0)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('bounds calculation with transformed geometry', () => {
|
|
186
|
+
const rect = new Rectangle2d({
|
|
187
|
+
width: 50,
|
|
188
|
+
height: 50,
|
|
189
|
+
isFilled: true,
|
|
190
|
+
}).transform(Mat.Translate(100, 100))
|
|
191
|
+
|
|
192
|
+
const excludedRect = new Rectangle2d({
|
|
193
|
+
width: 200,
|
|
194
|
+
height: 200,
|
|
195
|
+
isFilled: true,
|
|
196
|
+
excludeFromShapeBounds: true,
|
|
197
|
+
}).transform(Mat.Translate(50, 50))
|
|
198
|
+
|
|
199
|
+
const group = new Group2d({
|
|
200
|
+
children: [rect, excludedRect],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// The bounds should only include the non-excluded rectangle
|
|
204
|
+
const bounds = group.bounds
|
|
205
|
+
// Verify that the excluded rectangle doesn't affect the bounds
|
|
206
|
+
// The bounds should be smaller than if the excluded rect was included
|
|
207
|
+
expect(bounds.width).toBeLessThan(200) // Should not include the excluded rect's width
|
|
208
|
+
expect(bounds.height).toBeLessThan(200) // Should not include the excluded rect's height
|
|
209
|
+
// The bounds should not be empty
|
|
210
|
+
expect(bounds.width).toBeGreaterThan(0)
|
|
211
|
+
expect(bounds.height).toBeGreaterThan(0)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('getBoundsVertices', () => {
|
|
216
|
+
test('basic geometry returns vertices when not excluded from bounds', () => {
|
|
217
|
+
const rect = new Rectangle2d({
|
|
218
|
+
width: 100,
|
|
219
|
+
height: 50,
|
|
220
|
+
isFilled: true,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const boundsVertices = rect.getBoundsVertices()
|
|
224
|
+
const vertices = rect.getVertices()
|
|
225
|
+
|
|
226
|
+
expect(boundsVertices).toEqual(vertices)
|
|
227
|
+
expect(boundsVertices.length).toBe(4)
|
|
228
|
+
expect(boundsVertices).toMatchObject([
|
|
229
|
+
{ x: 0, y: 0, z: 1 },
|
|
230
|
+
{ x: 100, y: 0, z: 1 },
|
|
231
|
+
{ x: 100, y: 50, z: 1 },
|
|
232
|
+
{ x: 0, y: 50, z: 1 },
|
|
233
|
+
])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('geometry excluded from shape bounds returns empty array', () => {
|
|
237
|
+
const rect = new Rectangle2d({
|
|
238
|
+
width: 100,
|
|
239
|
+
height: 50,
|
|
240
|
+
isFilled: true,
|
|
241
|
+
excludeFromShapeBounds: true,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const boundsVertices = rect.getBoundsVertices()
|
|
245
|
+
expect(boundsVertices).toEqual([])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('cached boundsVertices property', () => {
|
|
249
|
+
const rect = new Rectangle2d({
|
|
250
|
+
width: 100,
|
|
251
|
+
height: 50,
|
|
252
|
+
isFilled: true,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Access the cached property multiple times
|
|
256
|
+
const boundsVertices1 = rect.boundsVertices
|
|
257
|
+
const boundsVertices2 = rect.boundsVertices
|
|
258
|
+
|
|
259
|
+
// Should return the same reference (cached)
|
|
260
|
+
expect(boundsVertices1).toBe(boundsVertices2)
|
|
261
|
+
expect(boundsVertices1.length).toBe(4)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('TransformedGeometry2d getBoundsVertices', () => {
|
|
266
|
+
test('transforms bounds vertices correctly', () => {
|
|
267
|
+
const rect = new Rectangle2d({
|
|
268
|
+
width: 100,
|
|
269
|
+
height: 50,
|
|
270
|
+
isFilled: true,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const transformed = rect.transform(Mat.Translate(50, 100).scale(2, 2))
|
|
274
|
+
const boundsVertices = transformed.getBoundsVertices()
|
|
275
|
+
|
|
276
|
+
expect(boundsVertices).toMatchObject([
|
|
277
|
+
{ x: 50, y: 100, z: 1 },
|
|
278
|
+
{ x: 250, y: 100, z: 1 },
|
|
279
|
+
{ x: 250, y: 200, z: 1 },
|
|
280
|
+
{ x: 50, y: 200, z: 1 },
|
|
281
|
+
])
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('transforms empty bounds vertices for excluded geometry', () => {
|
|
285
|
+
const rect = new Rectangle2d({
|
|
286
|
+
width: 100,
|
|
287
|
+
height: 50,
|
|
288
|
+
isFilled: true,
|
|
289
|
+
excludeFromShapeBounds: true,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const transformed = rect.transform(Mat.Translate(50, 100))
|
|
293
|
+
const boundsVertices = transformed.getBoundsVertices()
|
|
294
|
+
|
|
295
|
+
expect(boundsVertices).toEqual([])
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('nested transform preserves bounds vertices behavior', () => {
|
|
299
|
+
const rect = new Rectangle2d({
|
|
300
|
+
width: 100,
|
|
301
|
+
height: 50,
|
|
302
|
+
isFilled: true,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const transformed1 = rect.transform(Mat.Translate(10, 20))
|
|
306
|
+
const transformed2 = transformed1.transform(Mat.Scale(2, 2))
|
|
307
|
+
const boundsVertices = transformed2.getBoundsVertices()
|
|
308
|
+
|
|
309
|
+
expect(boundsVertices).toMatchObject([
|
|
310
|
+
{ x: 20, y: 40, z: 1 },
|
|
311
|
+
{ x: 220, y: 40, z: 1 },
|
|
312
|
+
{ x: 220, y: 140, z: 1 },
|
|
313
|
+
{ x: 20, y: 140, z: 1 },
|
|
314
|
+
])
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('Group2d getBoundsVertices', () => {
|
|
319
|
+
test('flattens children bounds vertices', () => {
|
|
320
|
+
const rect1 = new Rectangle2d({
|
|
321
|
+
width: 50,
|
|
322
|
+
height: 50,
|
|
323
|
+
isFilled: true,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const rect2 = new Rectangle2d({
|
|
327
|
+
width: 30,
|
|
328
|
+
height: 30,
|
|
329
|
+
isFilled: true,
|
|
330
|
+
}).transform(Mat.Translate(60, 60))
|
|
331
|
+
|
|
332
|
+
const group = new Group2d({
|
|
333
|
+
children: [rect1, rect2],
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const boundsVertices = group.getBoundsVertices()
|
|
337
|
+
|
|
338
|
+
// Should include all vertices from both rectangles
|
|
339
|
+
expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
|
|
340
|
+
|
|
341
|
+
// Check that we have vertices from both rectangles
|
|
342
|
+
expect(boundsVertices).toEqual(
|
|
343
|
+
expect.arrayContaining([
|
|
344
|
+
expect.objectContaining({ x: 0, y: 0 }), // rect1 vertices
|
|
345
|
+
expect.objectContaining({ x: 50, y: 0 }),
|
|
346
|
+
expect.objectContaining({ x: 50, y: 50 }),
|
|
347
|
+
expect.objectContaining({ x: 0, y: 50 }),
|
|
348
|
+
expect.objectContaining({ x: 60, y: 60 }), // rect2 vertices
|
|
349
|
+
expect.objectContaining({ x: 90, y: 60 }),
|
|
350
|
+
expect.objectContaining({ x: 90, y: 90 }),
|
|
351
|
+
expect.objectContaining({ x: 60, y: 90 }),
|
|
352
|
+
])
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('excludes children marked as excluded from bounds', () => {
|
|
357
|
+
const rect1 = new Rectangle2d({
|
|
358
|
+
width: 50,
|
|
359
|
+
height: 50,
|
|
360
|
+
isFilled: true,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const rect2 = new Rectangle2d({
|
|
364
|
+
width: 100,
|
|
365
|
+
height: 100,
|
|
366
|
+
isFilled: true,
|
|
367
|
+
excludeFromShapeBounds: true,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const group = new Group2d({
|
|
371
|
+
children: [rect1, rect2],
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const boundsVertices = group.getBoundsVertices()
|
|
375
|
+
|
|
376
|
+
// Should only include vertices from rect1, not rect2
|
|
377
|
+
expect(boundsVertices.length).toBe(4) // Only rect1's 4 vertices
|
|
378
|
+
expect(boundsVertices).toMatchObject([
|
|
379
|
+
{ x: 0, y: 0, z: 1 },
|
|
380
|
+
{ x: 50, y: 0, z: 1 },
|
|
381
|
+
{ x: 50, y: 50, z: 1 },
|
|
382
|
+
{ x: 0, y: 50, z: 1 },
|
|
383
|
+
])
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('returns empty array when group itself is excluded from bounds', () => {
|
|
387
|
+
const rect1 = new Rectangle2d({
|
|
388
|
+
width: 50,
|
|
389
|
+
height: 50,
|
|
390
|
+
isFilled: true,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const rect2 = new Rectangle2d({
|
|
394
|
+
width: 30,
|
|
395
|
+
height: 30,
|
|
396
|
+
isFilled: true,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const group = new Group2d({
|
|
400
|
+
children: [rect1, rect2],
|
|
401
|
+
excludeFromShapeBounds: true,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const boundsVertices = group.getBoundsVertices()
|
|
405
|
+
expect(boundsVertices).toEqual([])
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('handles nested groups correctly', () => {
|
|
409
|
+
const rect1 = new Rectangle2d({
|
|
410
|
+
width: 50,
|
|
411
|
+
height: 50,
|
|
412
|
+
isFilled: true,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const rect2 = new Rectangle2d({
|
|
416
|
+
width: 30,
|
|
417
|
+
height: 30,
|
|
418
|
+
isFilled: true,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const innerGroup = new Group2d({
|
|
422
|
+
children: [rect2],
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const outerGroup = new Group2d({
|
|
426
|
+
children: [rect1, innerGroup],
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const boundsVertices = outerGroup.getBoundsVertices()
|
|
430
|
+
|
|
431
|
+
// Should include vertices from both rectangles
|
|
432
|
+
expect(boundsVertices.length).toBe(8) // 4 vertices from each rectangle
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('handles all children excluded from bounds', () => {
|
|
436
|
+
const rect1 = new Rectangle2d({
|
|
437
|
+
width: 50,
|
|
438
|
+
height: 50,
|
|
439
|
+
isFilled: true,
|
|
440
|
+
excludeFromShapeBounds: true,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const rect2 = new Rectangle2d({
|
|
444
|
+
width: 30,
|
|
445
|
+
height: 30,
|
|
446
|
+
isFilled: true,
|
|
447
|
+
excludeFromShapeBounds: true,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const group = new Group2d({
|
|
451
|
+
children: [rect1, rect2],
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const boundsVertices = group.getBoundsVertices()
|
|
455
|
+
expect(boundsVertices).toEqual([])
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
39
459
|
function expectApproxMatch(a: VecLike, b: VecLike) {
|
|
40
460
|
expect(a.x).toBeCloseTo(b.x, 0.0001)
|
|
41
461
|
expect(a.y).toBeCloseTo(b.y, 0.0001)
|