@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.4.1
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 +114 -47
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +7 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +181 -294
- 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/ClickManager/ClickManager.js +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
- 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/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.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/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.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/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.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/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +5 -1
- package/dist-cjs/lib/utils/deepLinks.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/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/lib/utils/sync/alerts.js +1 -1
- package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +114 -47
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +9 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +189 -295
- 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/ClickManager/ClickManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
- 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/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.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/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.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/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.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/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +5 -1
- package/dist-esm/lib/utils/deepLinks.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/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
- package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +13 -7
- package/src/index.ts +6 -1
- package/src/lib/TldrawEditor.tsx +16 -4
- package/src/lib/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +266 -383
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/editors.test.ts +76 -0
- package/src/lib/globals/editors.ts +53 -0
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +60 -16
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -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/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
- package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
- package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
- package/src/lib/primitives/geometry/Group2d.ts +63 -27
- package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
- package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +107 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +7 -1
- package/src/lib/utils/normalizeWheel.test.ts +39 -0
- package/src/lib/utils/normalizeWheel.ts +6 -0
- package/src/lib/utils/rotation.ts +4 -12
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
- package/src/lib/utils/sync/alerts.ts +1 -1
- package/src/version.ts +3 -3
- package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { TLAssetId, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
|
|
2
|
+
import { noop } from '@tldraw/utils'
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
4
|
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
|
5
5
|
import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
|
|
@@ -33,7 +33,9 @@ export function useLocalStore(
|
|
|
33
33
|
|
|
34
34
|
setState({ status: 'loading' })
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Keyed by asset id rather than record identity so that a record update doesn't mint
|
|
37
|
+
// another object url for the same blob; every url minted here is revoked on cleanup.
|
|
38
|
+
const objectURLCache = new Map<TLAssetId, Promise<string | null>>()
|
|
37
39
|
const assets: TLAssetStore = {
|
|
38
40
|
upload: async (asset, file) => {
|
|
39
41
|
await client.db.storeAsset(asset.id, file)
|
|
@@ -43,11 +45,14 @@ export function useLocalStore(
|
|
|
43
45
|
if (!asset.props.src) return null
|
|
44
46
|
|
|
45
47
|
if (asset.props.src.startsWith('asset:')) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
let objectURL = objectURLCache.get(asset.id)
|
|
49
|
+
if (!objectURL) {
|
|
50
|
+
objectURL = client.db
|
|
51
|
+
.getAsset(asset.id)
|
|
52
|
+
.then((blob) => (blob ? URL.createObjectURL(blob) : null))
|
|
53
|
+
objectURLCache.set(asset.id, objectURL)
|
|
54
|
+
}
|
|
55
|
+
return await objectURL
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
return asset.props.src
|
|
@@ -78,6 +83,9 @@ export function useLocalStore(
|
|
|
78
83
|
return () => {
|
|
79
84
|
isClosed = true
|
|
80
85
|
client.close()
|
|
86
|
+
for (const objectURL of objectURLCache.values()) {
|
|
87
|
+
objectURL.then((url) => url && URL.revokeObjectURL(url), noop)
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
}, [options, setState])
|
|
83
91
|
|
|
@@ -70,6 +70,9 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
|
|
|
70
70
|
const newEvent = new WheelEvent('wheel', e as any)
|
|
71
71
|
;(newEvent as any).isSpecialRedispatchedEvent = true
|
|
72
72
|
cvs.dispatchEvent(newEvent)
|
|
73
|
+
// Flag the original too: it keeps bubbling, and an enclosing pass-through root (the
|
|
74
|
+
// navigation panel around the minimap) would otherwise redispatch it a second time.
|
|
75
|
+
;(e as any).isSpecialRedispatchedEvent = true
|
|
73
76
|
})
|
|
74
77
|
|
|
75
78
|
// The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useComputed, useValue } from '@tldraw/state-react'
|
|
2
|
+
import { areArraysShallowEqual } from '@tldraw/utils'
|
|
2
3
|
import { uniq } from '../utils/uniq'
|
|
3
4
|
import { useEditor } from './useEditor'
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@ export function usePeerIds() {
|
|
|
17
18
|
const $userIds = useComputed(
|
|
18
19
|
'userIds',
|
|
19
20
|
() => uniq(editor.getVisibleCollaborators().map((p) => p.userId)).sort(),
|
|
20
|
-
{ isEqual:
|
|
21
|
+
{ isEqual: areArraysShallowEqual },
|
|
21
22
|
[editor]
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -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,4 @@
|
|
|
1
|
-
import { Box } from './Box'
|
|
1
|
+
import { Box, rotateSelectionHandle } from './Box'
|
|
2
2
|
import { Vec } from './Vec'
|
|
3
3
|
|
|
4
4
|
describe('Box', () => {
|
|
@@ -759,3 +759,11 @@ describe('Box', () => {
|
|
|
759
759
|
})
|
|
760
760
|
})
|
|
761
761
|
})
|
|
762
|
+
|
|
763
|
+
describe('rotateSelectionHandle', () => {
|
|
764
|
+
it('wraps around for negative rotations', () => {
|
|
765
|
+
expect(rotateSelectionHandle('top', Math.PI / 2)).toBe('right')
|
|
766
|
+
expect(rotateSelectionHandle('top', -Math.PI / 2)).toBe('left')
|
|
767
|
+
expect(rotateSelectionHandle('top_left', -Math.PI)).toBe('bottom_right')
|
|
768
|
+
})
|
|
769
|
+
})
|
|
@@ -678,7 +678,9 @@ export function rotateSelectionHandle(handle: SelectionHandle, rotation: number)
|
|
|
678
678
|
const numSteps = Math.round(rotation / (PI / 4))
|
|
679
679
|
|
|
680
680
|
const currentIndex = ORDERED_SELECTION_HANDLES.indexOf(handle)
|
|
681
|
-
|
|
681
|
+
// numSteps is negative for negative rotations, so wrap the index into range
|
|
682
|
+
const n = ORDERED_SELECTION_HANDLES.length
|
|
683
|
+
return ORDERED_SELECTION_HANDLES[(((currentIndex + numSteps) % n) + n) % n]
|
|
682
684
|
}
|
|
683
685
|
|
|
684
686
|
/** @public */
|
|
@@ -336,6 +336,16 @@ describe('Vec.Slope', () => {
|
|
|
336
336
|
expect(Vec.Slope(new Vec(0, 0), new Vec(50, 100))).toEqual(2)
|
|
337
337
|
expect(Vec.Slope(new Vec(0, 0), new Vec(-50, 100))).toEqual(-2)
|
|
338
338
|
expect(Vec.Slope(new Vec(123, 456), new Vec(789, 24))).toEqual(-0.6486486486486487)
|
|
339
|
+
// A.x happened to equal B.y here, which used to trip the vertical guard
|
|
340
|
+
expect(Vec.Slope(new Vec(1, 0), new Vec(0, 1))).toEqual(-1)
|
|
341
|
+
expect(Vec.Slope(new Vec(5, 0), new Vec(5, 10))).toBeNaN()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('Vec.cross', () => {
|
|
346
|
+
it('Matches the static Vec.Cross.', () => {
|
|
347
|
+
expect(Vec.Cross(new Vec(1, 2, 3), new Vec(4, 5, 6))).toMatchObject({ x: -3, y: 6 })
|
|
348
|
+
expect(new Vec(1, 2, 3).cross(new Vec(4, 5, 6))).toMatchObject({ x: -3, y: 6 })
|
|
339
349
|
})
|
|
340
350
|
})
|
|
341
351
|
|
|
@@ -164,9 +164,9 @@ export class Vec {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
cross(V: VecLike) {
|
|
167
|
-
|
|
168
|
-
this.
|
|
169
|
-
|
|
167
|
+
const { x, y, z } = this
|
|
168
|
+
this.x = y * V.z! - z * V.y
|
|
169
|
+
this.y = z * V.x - x * V.z!
|
|
170
170
|
return this
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -497,7 +497,7 @@ export class Vec {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
static Slope(A: VecLike, B: VecLike): number {
|
|
500
|
-
if (A.x === B.
|
|
500
|
+
if (A.x === B.x) return NaN
|
|
501
501
|
return (A.y - B.y) / (A.x - B.x)
|
|
502
502
|
}
|
|
503
503
|
|
|
@@ -1,5 +1,228 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Arc2d } from './Arc2d'
|
|
3
|
+
|
|
4
|
+
// A quarter circle of radius 10 centred at the origin, from (10, 0) clockwise (in screen
|
|
5
|
+
// space, i.e. with increasing angle) to (0, 10).
|
|
6
|
+
const quarter = () =>
|
|
7
|
+
new Arc2d({
|
|
8
|
+
center: new Vec(0, 0),
|
|
9
|
+
start: new Vec(10, 0),
|
|
10
|
+
end: new Vec(0, 10),
|
|
11
|
+
sweepFlag: 1,
|
|
12
|
+
largeArcFlag: 0,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// The complementary three-quarter arc between the same points, going the other way round.
|
|
16
|
+
const threeQuarter = () =>
|
|
17
|
+
new Arc2d({
|
|
18
|
+
center: new Vec(0, 0),
|
|
19
|
+
start: new Vec(10, 0),
|
|
20
|
+
end: new Vec(0, 10),
|
|
21
|
+
sweepFlag: 0,
|
|
22
|
+
largeArcFlag: 1,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function expectVec(actual: Vec, expected: { x: number; y: number }) {
|
|
26
|
+
expect(actual.x).toBeCloseTo(expected.x)
|
|
27
|
+
expect(actual.y).toBeCloseTo(expected.y)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Arc2d construction', () => {
|
|
31
|
+
it('is never closed or filled', () => {
|
|
32
|
+
expect(quarter()).toMatchObject({ isClosed: false, isFilled: false })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('throws when start and end are the same point', () => {
|
|
36
|
+
expect(
|
|
37
|
+
() =>
|
|
38
|
+
new Arc2d({
|
|
39
|
+
center: new Vec(0, 0),
|
|
40
|
+
start: new Vec(10, 0),
|
|
41
|
+
end: new Vec(10, 0),
|
|
42
|
+
sweepFlag: 1,
|
|
43
|
+
largeArcFlag: 0,
|
|
44
|
+
})
|
|
45
|
+
).toThrow('Arc must have different start and end points.')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('Arc2d.getLength', () => {
|
|
50
|
+
it('is the arc measure times the radius', () => {
|
|
51
|
+
expect(quarter().length).toBeCloseTo((Math.PI / 2) * 10)
|
|
52
|
+
expect(threeQuarter().length).toBeCloseTo(((3 * Math.PI) / 2) * 10)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('is half a circumference for a semicircle in either direction', () => {
|
|
56
|
+
const semi = new Arc2d({
|
|
57
|
+
center: new Vec(25, 25),
|
|
58
|
+
start: new Vec(25, 50),
|
|
59
|
+
end: new Vec(25, 0),
|
|
60
|
+
sweepFlag: 1,
|
|
61
|
+
largeArcFlag: 1,
|
|
62
|
+
})
|
|
63
|
+
expect(semi.length).toBeCloseTo(Math.PI * 25)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('Arc2d.getVertices', () => {
|
|
68
|
+
it('samples at least eight segments from start to end', () => {
|
|
69
|
+
const { vertices } = quarter()
|
|
70
|
+
expect(vertices.length).toBe(9)
|
|
71
|
+
expectVec(vertices[0], { x: 10, y: 0 })
|
|
72
|
+
expectVec(vertices[4], { x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
73
|
+
expectVec(vertices[8], { x: 0, y: 10 })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps every vertex on the circle', () => {
|
|
77
|
+
for (const v of quarter().vertices) {
|
|
78
|
+
expect(Vec.Dist(v, new Vec(0, 0))).toBeCloseTo(10)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('walks the long way round for a large arc', () => {
|
|
83
|
+
const { vertices } = threeQuarter()
|
|
84
|
+
expect(vertices.length).toBe(9)
|
|
85
|
+
expectVec(vertices[0], { x: 10, y: 0 })
|
|
86
|
+
// the midpoint of the long arc is opposite the midpoint of the short arc
|
|
87
|
+
expectVec(vertices[4], { x: -10 / Math.SQRT2, y: -10 / Math.SQRT2 })
|
|
88
|
+
expectVec(vertices[8], { x: 0, y: 10 })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('samples more vertices for longer arcs', () => {
|
|
92
|
+
const big = new Arc2d({
|
|
93
|
+
center: new Vec(0, 0),
|
|
94
|
+
start: new Vec(100, 0),
|
|
95
|
+
end: new Vec(0, 100),
|
|
96
|
+
sweepFlag: 1,
|
|
97
|
+
largeArcFlag: 0,
|
|
98
|
+
})
|
|
99
|
+
// length is 50π ≈ 157, one vertex per 20 units -> 8 segments
|
|
100
|
+
expect(big.vertices.length).toBe(9)
|
|
101
|
+
const bigger = new Arc2d({
|
|
102
|
+
center: new Vec(0, 0),
|
|
103
|
+
start: new Vec(200, 0),
|
|
104
|
+
end: new Vec(0, 200),
|
|
105
|
+
sweepFlag: 1,
|
|
106
|
+
largeArcFlag: 0,
|
|
107
|
+
})
|
|
108
|
+
// length is 100π ≈ 314 -> 16 segments
|
|
109
|
+
expect(bigger.vertices.length).toBe(17)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('Arc2d.bounds', () => {
|
|
114
|
+
it('is the box around the sampled vertices', () => {
|
|
115
|
+
const { bounds } = quarter()
|
|
116
|
+
expect(bounds.x).toBeCloseTo(0)
|
|
117
|
+
expect(bounds.y).toBeCloseTo(0)
|
|
118
|
+
expect(bounds.w).toBeCloseTo(10)
|
|
119
|
+
expect(bounds.h).toBeCloseTo(10)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Arc2d.nearestPoint', () => {
|
|
124
|
+
it('projects radially onto the arc when the angle is within the sweep', () => {
|
|
125
|
+
expectVec(quarter().nearestPoint(new Vec(5, 5)), { x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
126
|
+
expectVec(quarter().nearestPoint(new Vec(20, 20)), {
|
|
127
|
+
x: 10 / Math.SQRT2,
|
|
128
|
+
y: 10 / Math.SQRT2,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns the start point for angles before the arc', () => {
|
|
133
|
+
expect(quarter().nearestPoint(new Vec(20, -5))).toMatchObject({ x: 10, y: 0 })
|
|
134
|
+
expect(quarter().nearestPoint(new Vec(20, 0))).toMatchObject({ x: 10, y: 0 })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('returns the end point for angles after the arc', () => {
|
|
138
|
+
expect(quarter().nearestPoint(new Vec(-5, 20))).toMatchObject({ x: 0, y: 10 })
|
|
139
|
+
expect(quarter().nearestPoint(new Vec(0, 20))).toMatchObject({ x: 0, y: 10 })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns the start point for the centre itself', () => {
|
|
143
|
+
expect(quarter().nearestPoint(new Vec(0, 0))).toMatchObject({ x: 10, y: 0 })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('projects onto the long arc', () => {
|
|
147
|
+
expectVec(threeQuarter().nearestPoint(new Vec(-20, 0)), { x: -10, y: 0 })
|
|
148
|
+
expectVec(threeQuarter().nearestPoint(new Vec(0, -3)), { x: 0, y: -10 })
|
|
149
|
+
// a point in the gap of the long arc snaps to the nearer end point
|
|
150
|
+
expect(threeQuarter().nearestPoint(new Vec(20, 5))).toMatchObject({ x: 10, y: 0 })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('Arc2d.distanceToPoint', () => {
|
|
155
|
+
it('measures to the arc or its end points', () => {
|
|
156
|
+
expect(quarter().distanceToPoint(new Vec(20, 20))).toBeCloseTo(Math.hypot(20, 20) - 10)
|
|
157
|
+
expect(quarter().distanceToPoint(new Vec(20, 0))).toBe(10)
|
|
158
|
+
expect(quarter().distanceToPoint(new Vec(0, 0))).toBe(10)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('is never negative because an arc has no interior', () => {
|
|
162
|
+
expect(quarter().distanceToPoint(new Vec(1, 1), true)).toBeGreaterThan(0)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('Arc2d.hitTestPoint', () => {
|
|
167
|
+
it('hits points on the arc within the margin', () => {
|
|
168
|
+
expect(quarter().hitTestPoint(new Vec(10 / Math.SQRT2, 10 / Math.SQRT2), 0.001)).toBe(true)
|
|
169
|
+
expect(quarter().hitTestPoint(new Vec(10, 0))).toBe(true)
|
|
170
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 3)).toBe(true)
|
|
171
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 2)).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('does not hit the sector inside the arc even with hitInside', () => {
|
|
175
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 0, true)).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('Arc2d.hitTestLineSegment', () => {
|
|
180
|
+
it('hits a segment crossing the arc', () => {
|
|
181
|
+
expect(quarter().hitTestLineSegment(new Vec(5, 0), new Vec(5, 20))).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('misses a segment crossing the circle outside the arc', () => {
|
|
185
|
+
// crosses the circle at (-5, 8.66), which is at 120° and not on the quarter arc
|
|
186
|
+
expect(quarter().hitTestLineSegment(new Vec(-5, 0), new Vec(-5, 20))).toBe(false)
|
|
187
|
+
// but it is on the complementary arc
|
|
188
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(-5, 0), new Vec(-5, 20))).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('misses a segment that does not reach the circle', () => {
|
|
192
|
+
expect(quarter().hitTestLineSegment(new Vec(20, 20), new Vec(30, 30))).toBe(false)
|
|
193
|
+
expect(quarter().hitTestLineSegment(new Vec(1, 1), new Vec(2, 2))).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Locks in current behaviour, see #10555.
|
|
197
|
+
it('hits crossings just beyond the end points (known quirk)', () => {
|
|
198
|
+
// (5, -8.66) is at -60°, off the quarter arc, but getPointInArcT clamps it to the start
|
|
199
|
+
expect(quarter().hitTestLineSegment(new Vec(5, -20), new Vec(5, 0))).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('checks both directions of the long arc', () => {
|
|
203
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(5, -20), new Vec(5, 0))).toBe(true)
|
|
204
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(5, 0), new Vec(5, 20))).toBe(false)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('Arc2d.getSvgPathData', () => {
|
|
209
|
+
it('emits a move and an arc command with the flags', () => {
|
|
210
|
+
expect(quarter().getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 0, 10')
|
|
211
|
+
expect(threeQuarter().getSvgPathData()).toBe('M10, 0 A10 10 0 1 0 0, 10')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('omits the move command when not first', () => {
|
|
215
|
+
expect(quarter().getSvgPathData(false)).toBe(' A10 10 0 0 1 0, 10')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('rounds coordinates to two decimals', () => {
|
|
219
|
+
const arc = new Arc2d({
|
|
220
|
+
center: new Vec(0, 0),
|
|
221
|
+
start: new Vec(10, 0),
|
|
222
|
+
end: new Vec(10 / Math.SQRT2, 10 / Math.SQRT2),
|
|
223
|
+
sweepFlag: 1,
|
|
224
|
+
largeArcFlag: 0,
|
|
225
|
+
})
|
|
226
|
+
expect(arc.getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 7.07, 7.07')
|
|
4
227
|
})
|
|
5
228
|
})
|