@tldraw/editor 5.3.1 → 5.4.0-canary.042da660fa66
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 +97 -52
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +3 -0
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +94 -26
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/globals/editors.js +57 -0
- package/dist-cjs/lib/globals/editors.js.map +7 -0
- package/dist-cjs/lib/hooks/useViewportHeight.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +9 -2
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +16 -3
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
- package/dist-cjs/lib/utils/normalizeWheel.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 +97 -52
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +4 -1
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +94 -26
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -4
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/globals/editors.mjs +37 -0
- package/dist-esm/lib/globals/editors.mjs.map +7 -0
- package/dist-esm/lib/hooks/useViewportHeight.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +9 -2
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
- package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +13 -7
- package/src/index.ts +7 -1
- package/src/lib/TldrawEditor.tsx +4 -1
- package/src/lib/editor/Editor.ts +136 -37
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +42 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +10 -8
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/globals/editors.test.ts +76 -0
- package/src/lib/globals/editors.ts +53 -0
- package/src/lib/hooks/useViewportHeight.ts +9 -1
- package/src/lib/license/LicenseManager.test.ts +160 -0
- package/src/lib/license/LicenseManager.ts +13 -4
- package/src/lib/license/LicenseProvider.tsx +24 -3
- package/src/lib/license/useLicenseManagerState.ts +8 -6
- package/src/lib/primitives/geometry/Group2d.ts +63 -27
- package/src/lib/utils/normalizeWheel.test.ts +39 -0
- package/src/lib/utils/normalizeWheel.ts +6 -0
- package/src/version.ts +3 -3
|
@@ -316,6 +316,42 @@ describe('LicenseManager', () => {
|
|
|
316
316
|
expect(result.isDomainValid).toBe(true)
|
|
317
317
|
})
|
|
318
318
|
|
|
319
|
+
it('Fails if the wildcard domain is a prefix of the current hostname', async () => {
|
|
320
|
+
// @ts-ignore
|
|
321
|
+
delete window.location
|
|
322
|
+
// @ts-ignore
|
|
323
|
+
window.location = new URL('https://sub.example.com.other.io')
|
|
324
|
+
|
|
325
|
+
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
326
|
+
permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
|
|
327
|
+
const permissiveLicenseKey = await generateLicenseKey(
|
|
328
|
+
JSON.stringify(permissiveHostsInfo),
|
|
329
|
+
keyPair
|
|
330
|
+
)
|
|
331
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
332
|
+
permissiveLicenseKey
|
|
333
|
+
)) as ValidLicenseKeyResult
|
|
334
|
+
expect(result.isDomainValid).toBe(false)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('Fails if the current hostname only ends with the wildcard domain', async () => {
|
|
338
|
+
// @ts-ignore
|
|
339
|
+
delete window.location
|
|
340
|
+
// @ts-ignore
|
|
341
|
+
window.location = new URL('https://notexample.com')
|
|
342
|
+
|
|
343
|
+
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
344
|
+
permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
|
|
345
|
+
const permissiveLicenseKey = await generateLicenseKey(
|
|
346
|
+
JSON.stringify(permissiveHostsInfo),
|
|
347
|
+
keyPair
|
|
348
|
+
)
|
|
349
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
350
|
+
permissiveLicenseKey
|
|
351
|
+
)) as ValidLicenseKeyResult
|
|
352
|
+
expect(result.isDomainValid).toBe(false)
|
|
353
|
+
})
|
|
354
|
+
|
|
319
355
|
it('Fails if has a subdomain wildcard isnt for the same base domain', async () => {
|
|
320
356
|
// @ts-ignore
|
|
321
357
|
delete window.location
|
|
@@ -422,6 +458,107 @@ describe('LicenseManager', () => {
|
|
|
422
458
|
expect(result.isDomainValid).toBe(true)
|
|
423
459
|
})
|
|
424
460
|
|
|
461
|
+
it('Is not development mode on a custom protocol', async () => {
|
|
462
|
+
process.env.NODE_ENV = 'production'
|
|
463
|
+
try {
|
|
464
|
+
// @ts-ignore
|
|
465
|
+
delete window.location
|
|
466
|
+
// @ts-ignore
|
|
467
|
+
window.location = new URL('app-bundle://app/index.html')
|
|
468
|
+
|
|
469
|
+
const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
470
|
+
nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
|
|
471
|
+
nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
|
|
472
|
+
const nativeLicenseKey = await generateLicenseKey(
|
|
473
|
+
JSON.stringify(nativeLicenseInfo),
|
|
474
|
+
keyPair
|
|
475
|
+
)
|
|
476
|
+
const nativeLicenseManager = new LicenseManager('', keyPair.publicKey)
|
|
477
|
+
const result = (await nativeLicenseManager.getLicenseFromKey(
|
|
478
|
+
nativeLicenseKey
|
|
479
|
+
)) as ValidLicenseKeyResult
|
|
480
|
+
expect(result).toMatchObject({
|
|
481
|
+
isLicenseParseable: true,
|
|
482
|
+
isNativeLicense: true,
|
|
483
|
+
isDomainValid: true,
|
|
484
|
+
isDevelopment: false,
|
|
485
|
+
})
|
|
486
|
+
expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe('licensed')
|
|
487
|
+
} finally {
|
|
488
|
+
process.env.NODE_ENV = 'test'
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('Validates the domain on a custom protocol', async () => {
|
|
493
|
+
process.env.NODE_ENV = 'production'
|
|
494
|
+
try {
|
|
495
|
+
// @ts-ignore
|
|
496
|
+
delete window.location
|
|
497
|
+
// @ts-ignore
|
|
498
|
+
window.location = new URL('app-bundle://app/index.html')
|
|
499
|
+
|
|
500
|
+
const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
501
|
+
const customProtocolLicenseManager = new LicenseManager('', keyPair.publicKey)
|
|
502
|
+
const result = (await customProtocolLicenseManager.getLicenseFromKey(
|
|
503
|
+
licenseKey
|
|
504
|
+
)) as ValidLicenseKeyResult
|
|
505
|
+
expect(result).toMatchObject({ isDomainValid: false, isDevelopment: false })
|
|
506
|
+
expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
|
|
507
|
+
'unlicensed-production'
|
|
508
|
+
)
|
|
509
|
+
} finally {
|
|
510
|
+
process.env.NODE_ENV = 'test'
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('Is development mode over http on a non-loopback host', async () => {
|
|
515
|
+
process.env.NODE_ENV = 'production'
|
|
516
|
+
try {
|
|
517
|
+
// @ts-ignore
|
|
518
|
+
delete window.location
|
|
519
|
+
// @ts-ignore
|
|
520
|
+
window.location = new URL('http://www.example.com')
|
|
521
|
+
|
|
522
|
+
const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
523
|
+
const httpLicenseManager = new LicenseManager('', keyPair.publicKey)
|
|
524
|
+
const result = (await httpLicenseManager.getLicenseFromKey(
|
|
525
|
+
licenseKey
|
|
526
|
+
)) as ValidLicenseKeyResult
|
|
527
|
+
expect(result.isDevelopment).toBe(true)
|
|
528
|
+
} finally {
|
|
529
|
+
process.env.NODE_ENV = 'test'
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it.each([
|
|
534
|
+
['http://localhost:5173/', true],
|
|
535
|
+
['http://127.0.0.1:3000/', true],
|
|
536
|
+
['https://localhost:8443/', true],
|
|
537
|
+
['https://[::1]:8443/', true],
|
|
538
|
+
['http://192.168.1.5:3000/', true],
|
|
539
|
+
['http://staging-box/', true],
|
|
540
|
+
['https://www.example.com/', false],
|
|
541
|
+
['tauri://localhost/index.html', false],
|
|
542
|
+
['http://tauri.localhost/index.html', false],
|
|
543
|
+
['app://tauri.localhost/com.example.app', false],
|
|
544
|
+
['app-bundle://localhost/index.html', false],
|
|
545
|
+
])('Development mode at %s is %s', async (href, expected) => {
|
|
546
|
+
process.env.NODE_ENV = 'production'
|
|
547
|
+
try {
|
|
548
|
+
// @ts-ignore
|
|
549
|
+
delete window.location
|
|
550
|
+
// @ts-ignore
|
|
551
|
+
window.location = new URL(href)
|
|
552
|
+
|
|
553
|
+
const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
554
|
+
const manager = new LicenseManager('', keyPair.publicKey)
|
|
555
|
+
const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
|
|
556
|
+
expect(result.isDevelopment).toBe(expected)
|
|
557
|
+
} finally {
|
|
558
|
+
process.env.NODE_ENV = 'test'
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
|
|
425
562
|
it('Fails if it is a native app with the wrong protocol', async () => {
|
|
426
563
|
// @ts-ignore
|
|
427
564
|
delete window.location
|
|
@@ -437,6 +574,29 @@ describe('LicenseManager', () => {
|
|
|
437
574
|
)) as ValidLicenseKeyResult
|
|
438
575
|
expect(result.isDomainValid).toBe(false)
|
|
439
576
|
})
|
|
577
|
+
|
|
578
|
+
it.each(['tauri://localhost/index.html', 'http://tauri.localhost/index.html'])(
|
|
579
|
+
'Does not license an unrelated key at %s',
|
|
580
|
+
async (href) => {
|
|
581
|
+
process.env.NODE_ENV = 'production'
|
|
582
|
+
try {
|
|
583
|
+
// @ts-ignore
|
|
584
|
+
delete window.location
|
|
585
|
+
// @ts-ignore
|
|
586
|
+
window.location = new URL(href)
|
|
587
|
+
|
|
588
|
+
const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
589
|
+
const manager = new LicenseManager('', keyPair.publicKey)
|
|
590
|
+
const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
|
|
591
|
+
expect(result).toMatchObject({ isDevelopment: false, isDomainValid: false })
|
|
592
|
+
expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
|
|
593
|
+
'unlicensed-production'
|
|
594
|
+
)
|
|
595
|
+
} finally {
|
|
596
|
+
process.env.NODE_ENV = 'test'
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
)
|
|
440
600
|
})
|
|
441
601
|
|
|
442
602
|
describe('License types and flags', () => {
|
|
@@ -174,10 +174,17 @@ export class LicenseManager {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
private getIsDevelopment() {
|
|
177
|
-
|
|
177
|
+
const protocol = window.location.protocol
|
|
178
|
+
const hostname = window.location.hostname
|
|
179
|
+
|
|
180
|
+
// Tauri uses `tauri://localhost` on macOS and Linux and `http://tauri.localhost` on Windows.
|
|
181
|
+
if (hostname.toLowerCase().endsWith('.localhost')) {
|
|
182
|
+
return process.env.NODE_ENV !== 'production'
|
|
183
|
+
}
|
|
184
|
+
|
|
178
185
|
return (
|
|
179
|
-
|
|
180
|
-
this.isLoopbackHost(
|
|
186
|
+
protocol === 'http:' ||
|
|
187
|
+
(protocol === 'https:' && this.isLoopbackHost(hostname)) ||
|
|
181
188
|
process.env.NODE_ENV !== 'production'
|
|
182
189
|
)
|
|
183
190
|
}
|
|
@@ -407,7 +414,9 @@ export class LicenseManager {
|
|
|
407
414
|
|
|
408
415
|
// Glob testing, we only support '*.somedomain.com' right now.
|
|
409
416
|
if (host.includes('*')) {
|
|
410
|
-
const globToRegex = new RegExp(
|
|
417
|
+
const globToRegex = new RegExp(
|
|
418
|
+
normalizedHostOrUrlRegex.replace(/\./g, '\\.').replace(/\*/g, '.*?') + '$'
|
|
419
|
+
)
|
|
411
420
|
return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)
|
|
412
421
|
}
|
|
413
422
|
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
2
|
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
|
3
|
+
import { useMaybeEditor } from '../hooks/useEditor'
|
|
3
4
|
import { LicenseManager } from './LicenseManager'
|
|
4
5
|
|
|
5
6
|
/** @internal */
|
|
6
|
-
export const LicenseContext = createContext
|
|
7
|
+
export const LicenseContext = createContext<LicenseManager | null>(null)
|
|
7
8
|
|
|
8
9
|
/** @internal */
|
|
9
|
-
export function useLicenseContext() {
|
|
10
|
-
|
|
10
|
+
export function useLicenseContext(): LicenseManager {
|
|
11
|
+
const licenseManager = useMaybeLicenseManager()
|
|
12
|
+
if (!licenseManager) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components, or inside an <EditorProvider /> wrapping an editor created by them'
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
return licenseManager
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the license manager for the current editor, or `null` if there is none. Reads the
|
|
22
|
+
* license context when inside `<TldrawEditor />`, and otherwise falls back to the license manager
|
|
23
|
+
* of the nearest editor, so UI mounted outside the editor tree (via `EditorProvider`) resolves the
|
|
24
|
+
* same license as the editor it belongs to.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function useMaybeLicenseManager(): LicenseManager | null {
|
|
29
|
+
const licenseManager = useContext(LicenseContext)
|
|
30
|
+
const editor = useMaybeEditor()
|
|
31
|
+
return licenseManager ?? editor?.licenseManager ?? null
|
|
11
32
|
}
|
|
12
33
|
|
|
13
34
|
function shouldHideEditorAfterDelay(licenseState: string): boolean {
|
|
@@ -8,16 +8,18 @@ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseS
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Reactively reads whether a licensable feature is enabled for the current license. Re-renders when
|
|
11
|
-
* license validation resolves.
|
|
11
|
+
* license validation resolves. Returns `false` when there is no license manager, so gated UI stays
|
|
12
|
+
* hidden when mounted without an editor or license context.
|
|
12
13
|
*
|
|
13
14
|
* @internal
|
|
14
15
|
*/
|
|
15
16
|
export function useLicenseFeatureFlag(
|
|
16
|
-
licenseManager: LicenseManager,
|
|
17
|
+
licenseManager: LicenseManager | null,
|
|
17
18
|
feature: LicenseFeatureName
|
|
18
19
|
): boolean {
|
|
19
|
-
return useValue(
|
|
20
|
-
|
|
21
|
-
feature,
|
|
22
|
-
|
|
20
|
+
return useValue(
|
|
21
|
+
'licenseFeature',
|
|
22
|
+
() => (licenseManager ? licenseManager.isFeatureEnabled(feature) : false),
|
|
23
|
+
[licenseManager, feature]
|
|
24
|
+
)
|
|
23
25
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EMPTY_ARRAY } from '@tldraw/state'
|
|
2
1
|
import { assert, invLerp, lerp } from '@tldraw/utils'
|
|
3
2
|
import { Box } from '../Box'
|
|
4
3
|
import { Mat } from '../Mat'
|
|
@@ -36,9 +35,15 @@ export class Group2d extends Geometry2d {
|
|
|
36
35
|
|
|
37
36
|
override getVertices(filters: Geometry2dFilters): Vec[] {
|
|
38
37
|
if (this.isExcludedByFilter(filters)) return []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const vertices: Vec[] = []
|
|
39
|
+
for (const child of this.children) {
|
|
40
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
41
|
+
const childVertices = child.getVertices(filters)
|
|
42
|
+
for (let i = 0, n = childVertices.length; i < n; i++) {
|
|
43
|
+
vertices.push(childVertices[i])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return vertices
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
override nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
|
|
@@ -84,9 +89,11 @@ export class Group2d extends Geometry2d {
|
|
|
84
89
|
hitInside: boolean,
|
|
85
90
|
filters = Geometry2dFilters.EXCLUDE_LABELS
|
|
86
91
|
): boolean {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
for (const child of this.children) {
|
|
93
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
94
|
+
if (child.hitTestPoint(point, margin, hitInside)) return true
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
override hitTestLineSegment(
|
|
@@ -95,42 +102,71 @@ export class Group2d extends Geometry2d {
|
|
|
95
102
|
zoom: number,
|
|
96
103
|
filters = Geometry2dFilters.EXCLUDE_LABELS
|
|
97
104
|
): boolean {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
for (const child of this.children) {
|
|
106
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
107
|
+
if (child.hitTestLineSegment(A, B, zoom)) return true
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const result: VecLike[] = []
|
|
114
|
+
for (const child of this.children) {
|
|
115
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
116
|
+
const hits = child.intersectLineSegment(A, B, filters)
|
|
117
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
118
|
+
result.push(hits[i])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
const result: VecLike[] = []
|
|
126
|
+
for (const child of this.children) {
|
|
127
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
128
|
+
const hits = child.intersectCircle(center, radius, filters)
|
|
129
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
130
|
+
result.push(hits[i])
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
override getBoundsVertices(): Vec[] {
|
|
118
137
|
if (this.excludeFromShapeBounds) return []
|
|
119
|
-
|
|
138
|
+
const vertices: Vec[] = []
|
|
139
|
+
for (const child of this.children) {
|
|
140
|
+
const childVertices = child.getBoundsVertices()
|
|
141
|
+
for (let i = 0, n = childVertices.length; i < n; i++) {
|
|
142
|
+
vertices.push(childVertices[i])
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return vertices
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
const result: VecLike[] = []
|
|
150
|
+
for (const child of this.children) {
|
|
151
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
152
|
+
const hits = child.intersectPolygon(polygon, filters)
|
|
153
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
154
|
+
result.push(hits[i])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result
|
|
127
158
|
}
|
|
128
159
|
|
|
129
160
|
override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
const result: VecLike[] = []
|
|
162
|
+
for (const child of this.children) {
|
|
163
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
164
|
+
const hits = child.intersectPolyline(polyline, filters)
|
|
165
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
166
|
+
result.push(hits[i])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
override interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { normalizeWheel } from './normalizeWheel'
|
|
2
|
+
|
|
3
|
+
function wheelEvent(props: Partial<WheelEvent>): WheelEvent {
|
|
4
|
+
return {
|
|
5
|
+
deltaX: 0,
|
|
6
|
+
deltaY: 0,
|
|
7
|
+
ctrlKey: false,
|
|
8
|
+
altKey: false,
|
|
9
|
+
metaKey: false,
|
|
10
|
+
shiftKey: false,
|
|
11
|
+
...props,
|
|
12
|
+
} as WheelEvent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('normalizeWheel', () => {
|
|
16
|
+
it('negates finite deltas for panning', () => {
|
|
17
|
+
const { x, y } = normalizeWheel(wheelEvent({ deltaX: 5, deltaY: 10 }))
|
|
18
|
+
expect(x).toBe(-5)
|
|
19
|
+
expect(y).toBe(-10)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('coerces a non-finite deltaX to zero movement', () => {
|
|
23
|
+
const { x, y } = normalizeWheel(wheelEvent({ deltaX: NaN, deltaY: 10 }))
|
|
24
|
+
expect(x === 0).toBe(true)
|
|
25
|
+
expect(y).toBe(-10)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('coerces a non-finite deltaY to zero movement', () => {
|
|
29
|
+
const { x, y } = normalizeWheel(wheelEvent({ deltaX: 5, deltaY: Infinity }))
|
|
30
|
+
expect(x).toBe(-5)
|
|
31
|
+
expect(y === 0).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('does not produce a non-finite zoom delta when wheeling with a bad deltaY', () => {
|
|
35
|
+
const { z } = normalizeWheel(wheelEvent({ deltaY: NaN, ctrlKey: true }))
|
|
36
|
+
expect(Number.isFinite(z)).toBe(true)
|
|
37
|
+
expect(z === 0).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -9,6 +9,12 @@ const MAX_ZOOM_STEP = 10
|
|
|
9
9
|
* @internal */
|
|
10
10
|
export function normalizeWheel(event: WheelEvent | React.WheelEvent<HTMLElement>) {
|
|
11
11
|
let { deltaY, deltaX } = event
|
|
12
|
+
|
|
13
|
+
// Coerce non-finite deltas to no movement so they can't propagate into the camera and
|
|
14
|
+
// crash validation. Also covers deltaZ below, which is derived from deltaY.
|
|
15
|
+
if (!Number.isFinite(deltaX)) deltaX = 0
|
|
16
|
+
if (!Number.isFinite(deltaY)) deltaY = 0
|
|
17
|
+
|
|
12
18
|
let deltaZ = 0
|
|
13
19
|
|
|
14
20
|
// wheeling
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.
|
|
4
|
+
export const version = '5.4.0-canary.042da660fa66'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-08-
|
|
8
|
-
patch: '2026-08-
|
|
7
|
+
minor: '2026-08-19T12:47:12.111Z',
|
|
8
|
+
patch: '2026-08-19T12:47:12.111Z',
|
|
9
9
|
}
|