@tldraw/editor 5.1.0-next.d7c83ba698ae → 5.1.0
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 +19 -2
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/editor/Editor.js +3 -5
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +23 -21
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +27 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +4 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +8 -0
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +7 -1
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +13 -15
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +43 -24
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +5 -0
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayUtil.js +5 -0
- package/dist-cjs/lib/editor/overlays/OverlayUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/options.js +1 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +1 -0
- package/dist-cjs/lib/utils/dom.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 +19 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/Editor.mjs +3 -5
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +23 -21
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +27 -11
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +8 -0
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +7 -1
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +13 -15
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +43 -24
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +5 -0
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayUtil.mjs +5 -0
- package/dist-esm/lib/editor/overlays/OverlayUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +1 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +1 -0
- package/dist-esm/lib/utils/dom.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/editor/Editor.ts +3 -5
- package/src/lib/editor/derivations/notVisibleShapes.ts +34 -26
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +132 -0
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +40 -12
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +12 -2
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -0
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +2 -2
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +33 -2
- package/src/lib/editor/managers/FontManager/FontManager.ts +20 -2
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +11 -2
- package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +13 -14
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +67 -40
- package/src/lib/editor/overlays/OverlayManager.ts +6 -0
- package/src/lib/editor/overlays/OverlayUtil.ts +5 -0
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
- package/src/lib/hooks/usePeerIds.ts +1 -0
- package/src/lib/options.ts +8 -0
- package/src/lib/utils/dom.ts +1 -0
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/collaboratorState.js +0 -42
- package/dist-cjs/lib/utils/collaboratorState.js.map +0 -7
- package/dist-esm/lib/utils/collaboratorState.mjs +0 -22
- package/dist-esm/lib/utils/collaboratorState.mjs.map +0 -7
- package/src/lib/utils/collaboratorState.ts +0 -54
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "5.1.0
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "5.1.0
|
|
53
|
-
"@tldraw/state-react": "5.1.0
|
|
54
|
-
"@tldraw/store": "5.1.0
|
|
55
|
-
"@tldraw/tlschema": "5.1.0
|
|
56
|
-
"@tldraw/utils": "5.1.0
|
|
57
|
-
"@tldraw/validate": "5.1.0
|
|
52
|
+
"@tldraw/state": "5.1.0",
|
|
53
|
+
"@tldraw/state-react": "5.1.0",
|
|
54
|
+
"@tldraw/store": "5.1.0",
|
|
55
|
+
"@tldraw/tlschema": "5.1.0",
|
|
56
|
+
"@tldraw/utils": "5.1.0",
|
|
57
|
+
"@tldraw/validate": "5.1.0",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -416,6 +416,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
416
416
|
})
|
|
417
417
|
|
|
418
418
|
this.fonts = new FontManager(this, fontAssetUrls)
|
|
419
|
+
this.disposables.add(() => this.fonts.dispose())
|
|
419
420
|
|
|
420
421
|
this.inputs = new InputsManager(this)
|
|
421
422
|
this.performance = new PerformanceManager(this)
|
|
@@ -502,6 +503,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
502
503
|
|
|
503
504
|
// Overlay utils
|
|
504
505
|
this.overlays = new OverlayManager(this)
|
|
506
|
+
this.disposables.add(() => this.overlays.dispose())
|
|
505
507
|
if (overlayUtilConstructors) {
|
|
506
508
|
for (const Util of overlayUtilConstructors) {
|
|
507
509
|
const util = new Util(this)
|
|
@@ -10370,11 +10372,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10370
10372
|
*/
|
|
10371
10373
|
blur({ blurContainer = true } = {}): this {
|
|
10372
10374
|
if (!this.getIsFocused()) return this
|
|
10373
|
-
|
|
10374
|
-
this.focusManager.blur()
|
|
10375
|
-
} else {
|
|
10376
|
-
this.complete() // stop any interaction
|
|
10377
|
-
}
|
|
10375
|
+
this.focusManager.blur({ blurContainer })
|
|
10378
10376
|
this.updateInstanceState({ isFocused: false })
|
|
10379
10377
|
return this
|
|
10380
10378
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed, isUninitialized } from '@tldraw/state'
|
|
2
|
-
import {
|
|
2
|
+
import { TLShapeId } from '@tldraw/tlschema'
|
|
3
3
|
import type { Editor } from '../Editor'
|
|
4
|
+
import { ShapeUtil } from '../shapes/ShapeUtil'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Non visible shapes are shapes outside of the viewport page bounds.
|
|
@@ -10,48 +11,55 @@ import type { Editor } from '../Editor'
|
|
|
10
11
|
*/
|
|
11
12
|
export function notVisibleShapes(editor: Editor) {
|
|
12
13
|
const emptySet = new Set<TLShapeId>()
|
|
14
|
+
const defaultCanCull = ShapeUtil.prototype.canCull
|
|
13
15
|
|
|
14
16
|
return computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {
|
|
15
|
-
const
|
|
17
|
+
const allShapeIds = editor.getCurrentPageShapeIds()
|
|
16
18
|
const viewportPageBounds = editor.getViewportPageBounds()
|
|
17
19
|
const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)
|
|
18
20
|
|
|
19
|
-
let shape: TLShape | undefined
|
|
20
|
-
|
|
21
21
|
// Fast path: if all shapes are visible, return empty set
|
|
22
|
-
if (visibleIds.size ===
|
|
22
|
+
if (visibleIds.size === allShapeIds.size) {
|
|
23
23
|
if (isUninitialized(prevValue) || prevValue.size > 0) {
|
|
24
24
|
return emptySet
|
|
25
25
|
}
|
|
26
26
|
return prevValue
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const notVisibleIds = new Set<TLShapeId>()
|
|
30
|
+
for (const id of allShapeIds) {
|
|
31
|
+
if (visibleIds.has(id)) continue
|
|
32
|
+
|
|
33
|
+
// Peek at the shape without subscribing — we only need its type to look up the util.
|
|
34
|
+
// Type is treated as immutable for a given id, so this is safe.
|
|
35
|
+
const peek = editor.store.unsafeGetWithoutCapture(id)
|
|
36
|
+
if (!peek) continue
|
|
37
|
+
const util = editor.getShapeUtil(peek.type)
|
|
38
|
+
|
|
39
|
+
// If canCull is the default (always-true), skip per-shape subscription entirely.
|
|
40
|
+
// >99% of shapes hit this path in practice.
|
|
41
|
+
if (util.canCull === defaultCanCull) {
|
|
42
|
+
notVisibleIds.add(id)
|
|
43
|
+
continue
|
|
37
44
|
}
|
|
38
|
-
|
|
45
|
+
|
|
46
|
+
// Custom canCull — subscribe so prop flips invalidate this derivation.
|
|
47
|
+
const shape = editor.getShape(id)
|
|
48
|
+
if (!shape) continue
|
|
49
|
+
if (!util.canCull(shape)) continue
|
|
50
|
+
notVisibleIds.add(id)
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
shape = allShapes[i]
|
|
45
|
-
if (visibleIds.has(shape.id)) continue
|
|
46
|
-
if (!editor.getShapeUtil(shape.type).canCull(shape)) continue
|
|
47
|
-
notVisibleIds.push(shape.id)
|
|
53
|
+
// First run
|
|
54
|
+
if (isUninitialized(prevValue)) {
|
|
55
|
+
return notVisibleIds
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
//
|
|
51
|
-
if (notVisibleIds.
|
|
58
|
+
// Reuse prev set when contents are unchanged
|
|
59
|
+
if (notVisibleIds.size === prevValue.size) {
|
|
52
60
|
let same = true
|
|
53
|
-
for (
|
|
54
|
-
if (!prevValue.has(
|
|
61
|
+
for (const id of notVisibleIds) {
|
|
62
|
+
if (!prevValue.has(id)) {
|
|
55
63
|
same = false
|
|
56
64
|
break
|
|
57
65
|
}
|
|
@@ -59,6 +67,6 @@ export function notVisibleShapes(editor: Editor) {
|
|
|
59
67
|
if (same) return prevValue
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
return
|
|
70
|
+
return notVisibleIds
|
|
63
71
|
})
|
|
64
72
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { PageRecordType, type TLInstancePresence } from '@tldraw/tlschema'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import type { Editor } from '../../Editor'
|
|
4
|
+
import { CollaboratorsManager } from './CollaboratorsManager'
|
|
5
|
+
|
|
6
|
+
const currentPageId = PageRecordType.createId('page')
|
|
7
|
+
|
|
8
|
+
function createPresence(userId: string): TLInstancePresence {
|
|
9
|
+
return {
|
|
10
|
+
typeName: 'instance_presence',
|
|
11
|
+
id: `instance_presence:${userId}` as TLInstancePresence['id'],
|
|
12
|
+
userId,
|
|
13
|
+
userName: userId,
|
|
14
|
+
lastActivityTimestamp: Date.now(),
|
|
15
|
+
color: '#000000',
|
|
16
|
+
camera: null,
|
|
17
|
+
selectedShapeIds: [],
|
|
18
|
+
currentPageId,
|
|
19
|
+
brush: null,
|
|
20
|
+
scribbles: [],
|
|
21
|
+
screenBounds: null,
|
|
22
|
+
followingUserId: null,
|
|
23
|
+
cursor: null,
|
|
24
|
+
chatMessage: '',
|
|
25
|
+
meta: {},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createEditor(presences: TLInstancePresence[] = []) {
|
|
30
|
+
const setInterval = vi.fn(() => 123)
|
|
31
|
+
const getInstanceState = vi.fn(() => ({
|
|
32
|
+
followingUserId: null,
|
|
33
|
+
highlightedUserIds: [],
|
|
34
|
+
}))
|
|
35
|
+
const userGetId = vi.fn(() => 'current-user')
|
|
36
|
+
|
|
37
|
+
const editor = {
|
|
38
|
+
options: {
|
|
39
|
+
collaboratorCheckIntervalMs: 1000,
|
|
40
|
+
collaboratorIdleTimeoutMs: 3000,
|
|
41
|
+
collaboratorInactiveTimeoutMs: 5000,
|
|
42
|
+
},
|
|
43
|
+
timers: {
|
|
44
|
+
setInterval,
|
|
45
|
+
},
|
|
46
|
+
user: {
|
|
47
|
+
getId: userGetId,
|
|
48
|
+
},
|
|
49
|
+
store: {
|
|
50
|
+
query: {
|
|
51
|
+
records: vi.fn(() => ({
|
|
52
|
+
get: () => presences,
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
getInstanceState,
|
|
57
|
+
getCurrentPageId: vi.fn(() => currentPageId),
|
|
58
|
+
} as unknown as Editor
|
|
59
|
+
|
|
60
|
+
return { editor, setInterval, getInstanceState, userGetId }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe(CollaboratorsManager, () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
vi.clearAllMocks()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('starts the visibility clock on the first visible collaborators read', () => {
|
|
69
|
+
const { editor, setInterval } = createEditor()
|
|
70
|
+
const manager = new CollaboratorsManager(editor)
|
|
71
|
+
|
|
72
|
+
expect(setInterval).not.toHaveBeenCalled()
|
|
73
|
+
|
|
74
|
+
expect(manager.getVisibleCollaborators()).toEqual([])
|
|
75
|
+
|
|
76
|
+
expect(setInterval).toHaveBeenCalledTimes(1)
|
|
77
|
+
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 1000)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('only starts the visibility clock once across repeated reads', () => {
|
|
81
|
+
const { editor, setInterval } = createEditor()
|
|
82
|
+
const manager = new CollaboratorsManager(editor)
|
|
83
|
+
|
|
84
|
+
manager.getVisibleCollaborators()
|
|
85
|
+
manager.getVisibleCollaborators()
|
|
86
|
+
manager.getVisibleCollaborators()
|
|
87
|
+
|
|
88
|
+
expect(setInterval).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('reads instance state once when filtering visible collaborators', () => {
|
|
92
|
+
const { editor, getInstanceState } = createEditor([
|
|
93
|
+
createPresence('user-1'),
|
|
94
|
+
createPresence('user-2'),
|
|
95
|
+
])
|
|
96
|
+
const manager = new CollaboratorsManager(editor)
|
|
97
|
+
|
|
98
|
+
expect(manager.getVisibleCollaborators()).toHaveLength(2)
|
|
99
|
+
|
|
100
|
+
expect(getInstanceState).toHaveBeenCalledTimes(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('hides idle collaborators that are following us', () => {
|
|
104
|
+
const presence = createPresence('peer')
|
|
105
|
+
presence.lastActivityTimestamp = Date.now() - 4000
|
|
106
|
+
presence.followingUserId = 'current-user'
|
|
107
|
+
const { editor } = createEditor([presence])
|
|
108
|
+
const manager = new CollaboratorsManager(editor)
|
|
109
|
+
|
|
110
|
+
expect(manager.getVisibleCollaborators()).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('shows idle collaborators that are following us when they have a chat message', () => {
|
|
114
|
+
const presence = createPresence('peer')
|
|
115
|
+
presence.lastActivityTimestamp = Date.now() - 4000
|
|
116
|
+
presence.followingUserId = 'current-user'
|
|
117
|
+
presence.chatMessage = 'hi'
|
|
118
|
+
const { editor } = createEditor([presence])
|
|
119
|
+
const manager = new CollaboratorsManager(editor)
|
|
120
|
+
|
|
121
|
+
expect(manager.getVisibleCollaborators()).toHaveLength(1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('shows idle collaborators that are not following us', () => {
|
|
125
|
+
const presence = createPresence('peer')
|
|
126
|
+
presence.lastActivityTimestamp = Date.now() - 4000
|
|
127
|
+
const { editor } = createEditor([presence])
|
|
128
|
+
const manager = new CollaboratorsManager(editor)
|
|
129
|
+
|
|
130
|
+
expect(manager.getVisibleCollaborators()).toHaveLength(1)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
|
|
2
|
-
import { TLInstancePresence } from '@tldraw/tlschema'
|
|
2
|
+
import type { TLInstancePresence } from '@tldraw/tlschema'
|
|
3
3
|
import { maxBy } from '@tldraw/utils'
|
|
4
|
-
import {
|
|
5
|
-
getCollaboratorStateFromElapsedTime,
|
|
6
|
-
shouldShowCollaborator,
|
|
7
|
-
} from '../../../utils/collaboratorState'
|
|
8
4
|
import type { Editor } from '../../Editor'
|
|
9
5
|
|
|
10
6
|
/**
|
|
@@ -17,12 +13,19 @@ import type { Editor } from '../../Editor'
|
|
|
17
13
|
* @public
|
|
18
14
|
*/
|
|
19
15
|
export class CollaboratorsManager {
|
|
20
|
-
constructor(private readonly editor: Editor) {
|
|
16
|
+
constructor(private readonly editor: Editor) {}
|
|
17
|
+
|
|
18
|
+
private _visibilityClockStarted = false
|
|
19
|
+
|
|
20
|
+
private _startVisibilityClock() {
|
|
21
|
+
if (this._visibilityClockStarted) return
|
|
22
|
+
this._visibilityClockStarted = true
|
|
23
|
+
|
|
21
24
|
// Editor disposes `editor.timers` on its own teardown, so the interval is
|
|
22
25
|
// automatically cleared when the editor is disposed.
|
|
23
|
-
editor.timers.setInterval(() => {
|
|
26
|
+
this.editor.timers.setInterval(() => {
|
|
24
27
|
this._visibilityClock.set(Date.now())
|
|
25
|
-
}, editor.options.collaboratorCheckIntervalMs)
|
|
28
|
+
}, this.editor.options.collaboratorCheckIntervalMs)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -75,14 +78,39 @@ export class CollaboratorsManager {
|
|
|
75
78
|
*/
|
|
76
79
|
@computed
|
|
77
80
|
getVisibleCollaborators(): TLInstancePresence[] {
|
|
81
|
+
const { editor } = this
|
|
82
|
+
const { collaboratorInactiveTimeoutMs, collaboratorIdleTimeoutMs } = editor.options
|
|
83
|
+
|
|
84
|
+
this._startVisibilityClock()
|
|
78
85
|
this._visibilityClock.get()
|
|
79
86
|
const now = Date.now()
|
|
80
|
-
|
|
87
|
+
const collaborators = this.getCollaborators()
|
|
88
|
+
if (!collaborators.length) return EMPTY_ARRAY
|
|
89
|
+
|
|
90
|
+
const { followingUserId, highlightedUserIds } = this.editor.getInstanceState()
|
|
91
|
+
const currentUserId = this.editor.user.getId()
|
|
92
|
+
|
|
93
|
+
return collaborators.filter((presence) => {
|
|
94
|
+
const { lastActivityTimestamp, userId, chatMessage } = presence
|
|
95
|
+
|
|
81
96
|
// Treat a missing `lastActivityTimestamp` as "active right now" (elapsed = 0)
|
|
82
97
|
// so newly-joined peers aren't immediately classified as idle/inactive.
|
|
83
|
-
const elapsed = Math.max(0, now - (
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
const elapsed = Math.max(0, now - (lastActivityTimestamp ?? now))
|
|
99
|
+
|
|
100
|
+
if (elapsed > collaboratorInactiveTimeoutMs) {
|
|
101
|
+
// Inactive: If they're inactive, only show if we're following them or they're highlighted
|
|
102
|
+
return followingUserId === userId || highlightedUserIds.includes(userId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (elapsed > collaboratorIdleTimeoutMs) {
|
|
106
|
+
// Idle: If they're idle and following us, hide them unless they have a chat message or are highlighted
|
|
107
|
+
if (presence.followingUserId === currentUserId) {
|
|
108
|
+
return !!(chatMessage || highlightedUserIds.includes(userId))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Active
|
|
113
|
+
return true
|
|
86
114
|
})
|
|
87
115
|
}
|
|
88
116
|
|
|
@@ -2,6 +2,10 @@ import { EASINGS } from '../../../primitives/easings'
|
|
|
2
2
|
import { Vec } from '../../../primitives/Vec'
|
|
3
3
|
import type { Editor } from '../../Editor'
|
|
4
4
|
|
|
5
|
+
// Tuned for touch/small-screen feel; adjust together with coarsePointerWidth.
|
|
6
|
+
const EDGE_SCROLL_SMALL_SCREEN_THRESHOLD_PX = 1000
|
|
7
|
+
const EDGE_SCROLL_SMALL_SCREEN_SPEED_FACTOR = 0.612
|
|
8
|
+
|
|
5
9
|
/** @public */
|
|
6
10
|
export class EdgeScrollManager {
|
|
7
11
|
constructor(public editor: Editor) {}
|
|
@@ -115,8 +119,14 @@ export class EdgeScrollManager {
|
|
|
115
119
|
const screenBounds = editor.getViewportScreenBounds()
|
|
116
120
|
|
|
117
121
|
// Determines how much the speed is affected by the screen size
|
|
118
|
-
const screenSizeFactorX =
|
|
119
|
-
|
|
122
|
+
const screenSizeFactorX =
|
|
123
|
+
screenBounds.w < EDGE_SCROLL_SMALL_SCREEN_THRESHOLD_PX
|
|
124
|
+
? EDGE_SCROLL_SMALL_SCREEN_SPEED_FACTOR
|
|
125
|
+
: 1
|
|
126
|
+
const screenSizeFactorY =
|
|
127
|
+
screenBounds.h < EDGE_SCROLL_SMALL_SCREEN_THRESHOLD_PX
|
|
128
|
+
? EDGE_SCROLL_SMALL_SCREEN_SPEED_FACTOR
|
|
129
|
+
: 1
|
|
120
130
|
|
|
121
131
|
// Determines the base speed of the scroll
|
|
122
132
|
const zoomLevel = editor.getZoomLevel()
|
|
@@ -334,6 +334,13 @@ describe('FocusManager', () => {
|
|
|
334
334
|
|
|
335
335
|
expect(callOrder).toEqual(['complete', 'blur'])
|
|
336
336
|
})
|
|
337
|
+
|
|
338
|
+
it('should complete without blurring the container when blurContainer is false', () => {
|
|
339
|
+
focusManager.blur({ blurContainer: false })
|
|
340
|
+
|
|
341
|
+
expect(editor.complete).toHaveBeenCalled()
|
|
342
|
+
expect(mockContainer.blur).not.toHaveBeenCalled()
|
|
343
|
+
})
|
|
337
344
|
})
|
|
338
345
|
|
|
339
346
|
describe('dispose', () => {
|
|
@@ -80,9 +80,9 @@ export class FocusManager {
|
|
|
80
80
|
this.editor.getContainer().focus()
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
blur() {
|
|
83
|
+
blur({ blurContainer = true } = {}) {
|
|
84
84
|
this.editor.complete() // stop any interaction
|
|
85
|
-
this.editor.getContainer().blur() // blur the container
|
|
85
|
+
if (blurContainer) this.editor.getContainer().blur() // blur the container
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
dispose() {
|
|
@@ -36,6 +36,8 @@ describe('FontManager', () => {
|
|
|
36
36
|
let editor: Mocked<Editor>
|
|
37
37
|
let fontManager: FontManager
|
|
38
38
|
let mockAssetUrls: { [key: string]: string }
|
|
39
|
+
let mockShapeFontFacesCacheGet: Mock
|
|
40
|
+
let mockShapeFontLoadStateCacheGet: Mock
|
|
39
41
|
|
|
40
42
|
const createMockFont = (overrides: Partial<TLFontFace> = {}): TLFontFace => ({
|
|
41
43
|
family: 'Test Font',
|
|
@@ -78,12 +80,15 @@ describe('FontManager', () => {
|
|
|
78
80
|
getFontFaces: vi.fn(() => []),
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
mockShapeFontFacesCacheGet = vi.fn(() => [])
|
|
84
|
+
mockShapeFontLoadStateCacheGet = vi.fn(() => ({ get: vi.fn(() => []) }))
|
|
85
|
+
|
|
81
86
|
const mockStore = {
|
|
82
87
|
createComputedCache: vi.fn(() => ({
|
|
83
|
-
get:
|
|
88
|
+
get: mockShapeFontFacesCacheGet,
|
|
84
89
|
})),
|
|
85
90
|
createCache: vi.fn(() => ({
|
|
86
|
-
get:
|
|
91
|
+
get: mockShapeFontLoadStateCacheGet,
|
|
87
92
|
})),
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -110,6 +115,32 @@ describe('FontManager', () => {
|
|
|
110
115
|
})
|
|
111
116
|
})
|
|
112
117
|
|
|
118
|
+
describe('dispose', () => {
|
|
119
|
+
it('clears font state and caches', async () => {
|
|
120
|
+
const font = createMockFont()
|
|
121
|
+
const shapeId = createShapeId('test')
|
|
122
|
+
mockShapeFontFacesCacheGet.mockReturnValue([font])
|
|
123
|
+
const firstPromise = fontManager.ensureFontIsLoaded(font)
|
|
124
|
+
|
|
125
|
+
expect(fontManager.getShapeFontFaces(shapeId)).toEqual([font])
|
|
126
|
+
fontManager.trackFontsForShape(shapeId)
|
|
127
|
+
fontManager.requestFonts([font])
|
|
128
|
+
await firstPromise
|
|
129
|
+
fontManager.dispose()
|
|
130
|
+
fontManager.requestFonts([font])
|
|
131
|
+
const secondPromise = fontManager.ensureFontIsLoaded(font)
|
|
132
|
+
|
|
133
|
+
expect(fontManager.getShapeFontFaces(shapeId)).toEqual([])
|
|
134
|
+
fontManager.trackFontsForShape(shapeId)
|
|
135
|
+
expect(mockShapeFontFacesCacheGet).toHaveBeenCalledTimes(1)
|
|
136
|
+
expect(mockShapeFontLoadStateCacheGet).toHaveBeenCalledTimes(1)
|
|
137
|
+
expect(queueMicrotask).toHaveBeenCalledTimes(2)
|
|
138
|
+
expect(secondPromise).not.toBe(firstPromise)
|
|
139
|
+
|
|
140
|
+
await secondPromise
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
113
144
|
describe('getShapeFontFaces', () => {
|
|
114
145
|
it('should return empty array when no fonts found', () => {
|
|
115
146
|
const shape = createMockShape()
|
|
@@ -16,6 +16,17 @@ interface FontState {
|
|
|
16
16
|
readonly loadingPromise: Promise<void>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
interface ShapeFontFacesCache {
|
|
20
|
+
get(id: TLShapeId): TLFontFace[] | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ShapeFontLoadStateCache {
|
|
24
|
+
get(id: TLShapeId): (FontState | null)[] | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }
|
|
28
|
+
const EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }
|
|
29
|
+
|
|
19
30
|
/** @public */
|
|
20
31
|
export class FontManager {
|
|
21
32
|
constructor(
|
|
@@ -49,8 +60,15 @@ export class FontManager {
|
|
|
49
60
|
)
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
dispose() {
|
|
64
|
+
this.fontStates.clear()
|
|
65
|
+
this.fontsToLoad.clear()
|
|
66
|
+
this.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE
|
|
67
|
+
this.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private shapeFontFacesCache: ShapeFontFacesCache
|
|
71
|
+
private shapeFontLoadStateCache: ShapeFontLoadStateCache
|
|
54
72
|
|
|
55
73
|
getShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {
|
|
56
74
|
const shapeId = typeof shape === 'string' ? shape : shape.id
|
|
@@ -7,6 +7,9 @@ import { isAccelKey } from '../../../utils/keyboard'
|
|
|
7
7
|
import type { Editor } from '../../Editor'
|
|
8
8
|
import { TLPinchEventInfo, TLPointerEventInfo, TLWheelEventInfo } from '../../types/event-types'
|
|
9
9
|
|
|
10
|
+
const POINTER_VELOCITY_REFERENCE_INTERVAL_MS = 16
|
|
11
|
+
const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
|
|
12
|
+
|
|
10
13
|
/** @public */
|
|
11
14
|
export class InputsManager {
|
|
12
15
|
constructor(private readonly editor: Editor) {}
|
|
@@ -473,8 +476,14 @@ export class InputsManager {
|
|
|
473
476
|
const length = delta.len()
|
|
474
477
|
const direction = length ? delta.div(length) : new Vec(0, 0)
|
|
475
478
|
|
|
476
|
-
//
|
|
477
|
-
const
|
|
479
|
+
// Preserve the old 16ms smoothing with alpha = 1 - (1 - 0.5)^(elapsed / 16).
|
|
480
|
+
const smoothing =
|
|
481
|
+
1 -
|
|
482
|
+
Math.pow(
|
|
483
|
+
1 - POINTER_VELOCITY_REFERENCE_SMOOTHING,
|
|
484
|
+
elapsed / POINTER_VELOCITY_REFERENCE_INTERVAL_MS
|
|
485
|
+
)
|
|
486
|
+
const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), smoothing)
|
|
478
487
|
|
|
479
488
|
// if the velocity is very small, just set it to 0
|
|
480
489
|
if (Math.abs(next.x) < 0.01) next.x = 0
|
|
@@ -113,25 +113,24 @@ export class RBushIndex {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Get
|
|
116
|
+
* Get the raw stored element for a shape, without allocating a Box.
|
|
117
|
+
* Use when you only need to read the indexed bounds for comparison.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
117
120
|
*/
|
|
118
|
-
|
|
119
|
-
return
|
|
121
|
+
getElement(id: TLShapeId): SpatialElement | undefined {
|
|
122
|
+
return this.elementsInTree.get(id)
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
126
|
+
* Iterate the entries currently in the index. Callers may upsert existing
|
|
127
|
+
* keys or remove keys during iteration; current callers do not insert new
|
|
128
|
+
* keys.
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
125
131
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!element) return undefined
|
|
129
|
-
return new Box(
|
|
130
|
-
element.minX,
|
|
131
|
-
element.minY,
|
|
132
|
-
element.maxX - element.minX,
|
|
133
|
-
element.maxY - element.minY
|
|
134
|
-
)
|
|
132
|
+
entries(): IterableIterator<[TLShapeId, SpatialElement]> {
|
|
133
|
+
return this.elementsInTree.entries()
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
/**
|