@tldraw/editor 5.4.0-canary.042da660fa66 → 5.4.0-canary.059d049864ed
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 +31 -1
- package/dist-cjs/index.js +1 -1
- 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 +139 -268
- package/dist-cjs/lib/editor/Editor.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/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -0
- 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/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 +9 -7
- 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/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/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.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/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +31 -1
- package/dist-esm/index.mjs +1 -1
- 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 +147 -269
- package/dist-esm/lib/editor/Editor.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/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -0
- 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/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 +9 -7
- 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/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/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.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/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- 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 +196 -341
- 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/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -0
- 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/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 +14 -7
- 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/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/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 +31 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/rotation.ts +4 -12
- package/src/version.ts +3 -3
package/src/lib/globals/menus.ts
CHANGED
|
@@ -106,10 +106,12 @@ export const tlmenus = {
|
|
|
106
106
|
* @public
|
|
107
107
|
*/
|
|
108
108
|
hideOpenMenus(contextId?: string) {
|
|
109
|
+
// ids read back from the store already carry their context suffix, so they are deleted
|
|
110
|
+
// (and later re-added) as-is rather than suffixed again
|
|
109
111
|
this._hiddenMenus = [...this.getOpenMenus(contextId)]
|
|
110
112
|
if (this._hiddenMenus.length === 0) return
|
|
111
113
|
for (const menu of this._hiddenMenus) {
|
|
112
|
-
this.deleteOpenMenu(menu
|
|
114
|
+
this.deleteOpenMenu(menu)
|
|
113
115
|
}
|
|
114
116
|
},
|
|
115
117
|
|
|
@@ -128,10 +130,15 @@ export const tlmenus = {
|
|
|
128
130
|
*/
|
|
129
131
|
showOpenMenus(contextId?: string) {
|
|
130
132
|
if (this._hiddenMenus.length === 0) return
|
|
133
|
+
const stillHidden: string[] = []
|
|
131
134
|
for (const menu of this._hiddenMenus) {
|
|
132
|
-
|
|
135
|
+
if (!contextId || menu.endsWith('-' + contextId)) {
|
|
136
|
+
this.addOpenMenu(menu)
|
|
137
|
+
} else {
|
|
138
|
+
stillHidden.push(menu)
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
|
-
this._hiddenMenus =
|
|
141
|
+
this._hiddenMenus = stillHidden
|
|
135
142
|
},
|
|
136
143
|
|
|
137
144
|
/**
|
|
@@ -148,7 +155,7 @@ export const tlmenus = {
|
|
|
148
155
|
* @public
|
|
149
156
|
*/
|
|
150
157
|
isMenuOpen(id: string, contextId?: string): boolean {
|
|
151
|
-
return this.getOpenMenus(contextId).includes(`${id}-${contextId}`)
|
|
158
|
+
return this.getOpenMenus(contextId).includes(contextId ? `${id}-${contextId}` : id)
|
|
152
159
|
},
|
|
153
160
|
|
|
154
161
|
/**
|
|
@@ -153,6 +153,7 @@ export function useCanvasEvents() {
|
|
|
153
153
|
if (e.dataTransfer?.files?.length) {
|
|
154
154
|
const files = Array.from(e.dataTransfer.files)
|
|
155
155
|
|
|
156
|
+
editor.markHistoryStoppingPoint('drop')
|
|
156
157
|
await editor.putExternalContent({
|
|
157
158
|
type: 'files',
|
|
158
159
|
files,
|
|
@@ -163,6 +164,7 @@ export function useCanvasEvents() {
|
|
|
163
164
|
|
|
164
165
|
const url = e.dataTransfer.getData('url')
|
|
165
166
|
if (url) {
|
|
167
|
+
editor.markHistoryStoppingPoint('drop')
|
|
166
168
|
await editor.putExternalContent({
|
|
167
169
|
type: 'url',
|
|
168
170
|
url,
|
|
@@ -29,8 +29,12 @@ export function useDocumentEvents() {
|
|
|
29
29
|
// re-dispatched, which would lead to an infinite loop.
|
|
30
30
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
31
31
|
preventDefault(e)
|
|
32
|
-
e.stopPropagation()
|
|
33
32
|
const cvs = container.querySelector('.tl-canvas')
|
|
33
|
+
// A drop on the canvas itself already reaches the canvas (and any shape's own React
|
|
34
|
+
// drop handlers) by bubbling; stopping it here would hide it from React, which
|
|
35
|
+
// delegates from the root above this container.
|
|
36
|
+
if (cvs?.contains(e.target as Node | null)) return
|
|
37
|
+
e.stopPropagation()
|
|
34
38
|
if (!cvs) return
|
|
35
39
|
const newEvent = new DragEvent(e.type, e)
|
|
36
40
|
;(newEvent as any).isSpecialRedispatchedEvent = true
|
|
@@ -277,10 +281,13 @@ export function useDocumentEvents() {
|
|
|
277
281
|
|
|
278
282
|
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
279
283
|
|
|
284
|
+
// Not the shared `preventDefault` reference: the DOM dedupes identical listeners, so with two
|
|
285
|
+
// editors in one document the first cleanup would otherwise remove it for both.
|
|
286
|
+
const handleGesture = (e: Event) => preventDefault(e)
|
|
280
287
|
const ownerDoc = container.ownerDocument
|
|
281
|
-
ownerDoc.addEventListener('gesturestart',
|
|
282
|
-
ownerDoc.addEventListener('gesturechange',
|
|
283
|
-
ownerDoc.addEventListener('gestureend',
|
|
288
|
+
ownerDoc.addEventListener('gesturestart', handleGesture)
|
|
289
|
+
ownerDoc.addEventListener('gesturechange', handleGesture)
|
|
290
|
+
ownerDoc.addEventListener('gestureend', handleGesture)
|
|
284
291
|
|
|
285
292
|
container.addEventListener('keydown', handleKeyDown)
|
|
286
293
|
container.addEventListener('keyup', handleKeyUp)
|
|
@@ -290,9 +297,9 @@ export function useDocumentEvents() {
|
|
|
290
297
|
|
|
291
298
|
container.removeEventListener('wheel', handleWheel)
|
|
292
299
|
|
|
293
|
-
ownerDoc.removeEventListener('gesturestart',
|
|
294
|
-
ownerDoc.removeEventListener('gesturechange',
|
|
295
|
-
ownerDoc.removeEventListener('gestureend',
|
|
300
|
+
ownerDoc.removeEventListener('gesturestart', handleGesture)
|
|
301
|
+
ownerDoc.removeEventListener('gesturechange', handleGesture)
|
|
302
|
+
ownerDoc.removeEventListener('gestureend', handleGesture)
|
|
296
303
|
|
|
297
304
|
container.removeEventListener('keydown', handleKeyDown)
|
|
298
305
|
container.removeEventListener('keyup', handleKeyUp)
|
|
@@ -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
|
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { getPointInArcT } from './utils'
|
|
1
|
+
import { areAnglesCompatible, getPointInArcT } from './utils'
|
|
2
2
|
|
|
3
3
|
describe('getPointInArcT', () => {
|
|
4
4
|
it('should return 0 for the start of the arc', () => {
|
|
@@ -57,3 +57,12 @@ describe('getPointInArcT', () => {
|
|
|
57
57
|
expect(getPointInArcT(mAB, A, B, P)).toBe(1)
|
|
58
58
|
})
|
|
59
59
|
})
|
|
60
|
+
|
|
61
|
+
describe('areAnglesCompatible', () => {
|
|
62
|
+
it('treats angles a multiple of PI/2 apart as compatible regardless of sign', () => {
|
|
63
|
+
expect(areAnglesCompatible(-Math.PI / 4, Math.PI / 4)).toBe(true)
|
|
64
|
+
expect(areAnglesCompatible(Math.PI / 2 - 1e-9, 0)).toBe(true)
|
|
65
|
+
expect(areAnglesCompatible(Math.PI, -Math.PI / 2)).toBe(true)
|
|
66
|
+
expect(areAnglesCompatible(0, Math.PI / 3)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -189,7 +189,10 @@ export function snapAngle(r: number, segments: number): number {
|
|
|
189
189
|
* @public
|
|
190
190
|
*/
|
|
191
191
|
export function areAnglesCompatible(a: number, b: number) {
|
|
192
|
-
|
|
192
|
+
// Reduce the difference, not each angle: a sign-preserving % on each made
|
|
193
|
+
// -PI/4 and PI/4 incompatible. A remainder just under PI/2 is a multiple too.
|
|
194
|
+
const d = Math.abs(a - b) % (Math.PI / 2)
|
|
195
|
+
return approximately(d, 0) || approximately(d, Math.PI / 2)
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react'
|
|
2
|
+
import { Editor } from '../editor/Editor'
|
|
3
|
+
import { TldrawEditor } from '../TldrawEditor'
|
|
4
|
+
|
|
5
|
+
function dropEvent(dataTransfer: { files: File[]; getData(type: string): string }) {
|
|
6
|
+
const event = new Event('drop', { bubbles: true, cancelable: true })
|
|
7
|
+
Object.defineProperty(event, 'dataTransfer', { value: dataTransfer })
|
|
8
|
+
return event
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('useCanvasEvents drop handling', () => {
|
|
12
|
+
let editor: Editor
|
|
13
|
+
let canvas: HTMLElement
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await act(async () => {
|
|
17
|
+
render(
|
|
18
|
+
<TldrawEditor
|
|
19
|
+
shapeUtils={[]}
|
|
20
|
+
bindingUtils={[]}
|
|
21
|
+
onMount={(e) => {
|
|
22
|
+
editor = e
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
canvas = document.querySelector('[data-testid="canvas"]') as HTMLElement
|
|
28
|
+
|
|
29
|
+
// Stand-ins for the default handlers: any undoable change will do.
|
|
30
|
+
editor.registerExternalContentHandler('files', async () => {
|
|
31
|
+
editor.createPage({ name: 'dropped' })
|
|
32
|
+
})
|
|
33
|
+
editor.registerExternalContentHandler('url', async () => {
|
|
34
|
+
editor.createPage({ name: 'dropped' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// An unmarked change before the drop, like a stroke the user just drew.
|
|
38
|
+
editor.createPage({ name: 'before drop' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
editor?.dispose()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function pageNames() {
|
|
46
|
+
return editor.getPages().map((p) => p.name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('makes a dropped file its own undo step', async () => {
|
|
50
|
+
await act(async () => {
|
|
51
|
+
canvas.dispatchEvent(
|
|
52
|
+
dropEvent({ files: [new File(['x'], 'x.png', { type: 'image/png' })], getData: () => '' })
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
expect(pageNames()).toEqual(['Page 1', 'before drop', 'dropped'])
|
|
56
|
+
|
|
57
|
+
editor.undo()
|
|
58
|
+
|
|
59
|
+
expect(pageNames()).toEqual(['Page 1', 'before drop'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('makes a dropped url its own undo step', async () => {
|
|
63
|
+
await act(async () => {
|
|
64
|
+
canvas.dispatchEvent(
|
|
65
|
+
dropEvent({ files: [], getData: (type) => (type === 'url' ? 'https://example.com' : '') })
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
expect(pageNames()).toEqual(['Page 1', 'before drop', 'dropped'])
|
|
69
|
+
|
|
70
|
+
editor.undo()
|
|
71
|
+
|
|
72
|
+
expect(pageNames()).toEqual(['Page 1', 'before drop'])
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { createTLStore } from '../config/createTLStore'
|
|
4
|
+
import { TldrawEditor } from '../TldrawEditor'
|
|
5
|
+
|
|
6
|
+
describe('useDocumentEvents drop handling', () => {
|
|
7
|
+
// The container's native drop listener used to stop propagation before the event reached
|
|
8
|
+
// React's root, so React onDrop handlers inside the canvas never fired.
|
|
9
|
+
it('lets a drop inside the canvas reach React drop handlers there', async () => {
|
|
10
|
+
const onDrop = vi.fn((e: React.DragEvent) => e.stopPropagation())
|
|
11
|
+
function DropTarget() {
|
|
12
|
+
return <div data-testid="drop-target" onDrop={onDrop} />
|
|
13
|
+
}
|
|
14
|
+
const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
|
|
15
|
+
await act(async () => {
|
|
16
|
+
render(<TldrawEditor store={store} components={{ OnTheCanvas: DropTarget }} />)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const target = document.querySelector('[data-testid="drop-target"]')!
|
|
20
|
+
expect(target.closest('.tl-canvas')).not.toBeNull()
|
|
21
|
+
const event = new Event('drop', { bubbles: true, cancelable: true })
|
|
22
|
+
Object.defineProperty(event, 'dataTransfer', { value: { files: [], getData: () => '' } })
|
|
23
|
+
act(() => {
|
|
24
|
+
target.dispatchEvent(event)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
expect(onDrop).toHaveBeenCalledTimes(1)
|
|
28
|
+
// the browser default (navigating to the dropped file) is still prevented
|
|
29
|
+
expect(event.defaultPrevented).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -22,11 +22,27 @@ function Culled() {
|
|
|
22
22
|
return <button ref={ref} data-testid="marker" />
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The shape the navigation panel has: a pass-through root wrapping another pass-through root (the
|
|
27
|
+
* minimap).
|
|
28
|
+
*/
|
|
29
|
+
function Nested() {
|
|
30
|
+
const outer = useRef<HTMLDivElement>(null)
|
|
31
|
+
const inner = useRef<HTMLButtonElement>(null)
|
|
32
|
+
usePassThroughWheelEvents(outer)
|
|
33
|
+
usePassThroughWheelEvents(inner)
|
|
34
|
+
return (
|
|
35
|
+
<div ref={outer}>
|
|
36
|
+
<button ref={inner} data-testid="marker" />
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
describe('usePassThroughWheelEvents', () => {
|
|
26
|
-
async function renderEditor() {
|
|
42
|
+
async function renderEditor(InFrontOfTheCanvas = Culled) {
|
|
27
43
|
const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
|
|
28
44
|
await act(async () => {
|
|
29
|
-
render(<TldrawEditor store={store} components={{ InFrontOfTheCanvas
|
|
45
|
+
render(<TldrawEditor store={store} components={{ InFrontOfTheCanvas }} />)
|
|
30
46
|
})
|
|
31
47
|
return document.querySelector(`.${TL_CONTAINER_CLASS}`)!
|
|
32
48
|
}
|
|
@@ -64,6 +80,13 @@ describe('usePassThroughWheelEvents', () => {
|
|
|
64
80
|
expect(wheelOverMarker(container)).toHaveLength(1)
|
|
65
81
|
})
|
|
66
82
|
|
|
83
|
+
// Issue #10441: the original event keeps bubbling after the inner root redispatches it, so
|
|
84
|
+
// without a flag on it the outer root would redispatch it again.
|
|
85
|
+
it('redispatches once when the element is inside another pass-through element', async () => {
|
|
86
|
+
const container = await renderEditor(Nested)
|
|
87
|
+
expect(wheelOverMarker(container)).toHaveLength(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
67
90
|
it('stops redispatching once the owner unmounts', async () => {
|
|
68
91
|
const container = await renderEditor()
|
|
69
92
|
const marker = container.querySelector('[data-testid="marker"]')!
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'
|
|
2
2
|
import { compact } from '@tldraw/utils'
|
|
3
3
|
import type { Editor } from '../editor/Editor'
|
|
4
|
-
import { Mat } from '../primitives/Mat'
|
|
5
4
|
import { canonicalizeRotation } from '../primitives/utils'
|
|
6
5
|
import { Vec, VecLike } from '../primitives/Vec'
|
|
7
6
|
|
|
@@ -75,18 +74,11 @@ export function applyRotationToSnapshotShapes({
|
|
|
75
74
|
// We need to both rotate each shape individually and rotate the shapes
|
|
76
75
|
// around the pivot point (the average center of all rotating shapes.)
|
|
77
76
|
|
|
78
|
-
const parentTransform = isShapeId(shape.parentId)
|
|
79
|
-
? editor.getShapePageTransform(shape.parentId)!
|
|
80
|
-
: Mat.Identity()
|
|
81
|
-
|
|
82
77
|
const newPagePoint = Vec.RotWith(initialPagePoint, centerOverride ?? initialPageCenter, delta)
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Mat.Inverse(parentTransform),
|
|
88
|
-
newPagePoint
|
|
89
|
-
)
|
|
79
|
+
// uses the current parent transform in case it has moved/resized since the start
|
|
80
|
+
// (e.g. if rotating a shape at the edge of a group)
|
|
81
|
+
const newLocalPoint = editor.getPointInParentSpace(shape.id, newPagePoint)
|
|
90
82
|
const newRotation = canonicalizeRotation(shape.rotation + delta)
|
|
91
83
|
|
|
92
84
|
return {
|
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.0-canary.
|
|
4
|
+
export const version = '5.4.0-canary.059d049864ed'
|
|
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-22T06:21:04.451Z',
|
|
8
|
+
patch: '2026-08-22T06:21:04.451Z',
|
|
9
9
|
}
|