@tldraw/editor 5.1.1 → 5.2.0-canary.019da1aa690a
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/README.md +7 -1
- package/dist-cjs/index.d.ts +52 -50
- package/dist-cjs/index.js +4 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +8 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +4 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js +2 -2
- package/dist-cjs/lib/components/default-components/DefaultLoadingScreen.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSvgDefs.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +121 -55
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -0
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/event-types.js +0 -2
- package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +14 -7
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +3 -1
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -2
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +5 -3
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
- package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
- package/dist-cjs/lib/utils/pointer.js +32 -0
- package/dist-cjs/lib/utils/pointer.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +52 -50
- package/dist-esm/index.mjs +5 -7
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +4 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs +2 -2
- package/dist-esm/lib/components/default-components/DefaultLoadingScreen.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultShapeErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs +2 -2
- package/dist-esm/lib/components/default-components/DefaultSvgDefs.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +121 -55
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -0
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/editor/types/event-types.mjs +0 -2
- package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +14 -7
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +3 -1
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -2
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +5 -3
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
- package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
- package/dist-esm/lib/utils/pointer.mjs +12 -0
- package/dist-esm/lib/utils/pointer.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +5 -3
- package/package.json +11 -8
- package/src/index.ts +2 -5
- package/src/lib/components/MenuClickCapture.tsx +8 -4
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +4 -1
- package/src/lib/components/default-components/DefaultLoadingScreen.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeErrorFallback.tsx +4 -3
- package/src/lib/components/default-components/DefaultSvgDefs.tsx +1 -1
- package/src/lib/editor/Editor.ts +168 -72
- package/src/lib/editor/derivations/bindingsIndex.ts +1 -1
- package/src/lib/editor/derivations/parentsToChildren.ts +1 -1
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +1 -1
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +13 -9
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
- package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
- package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -0
- package/src/lib/editor/tools/StateNode.ts +0 -2
- package/src/lib/editor/types/event-types.ts +2 -6
- package/src/lib/hooks/useCanvasEvents.ts +19 -12
- package/src/lib/hooks/usePresence.ts +2 -2
- package/src/lib/license/LicenseProvider.tsx +3 -1
- package/src/lib/primitives/utils.ts +1 -1
- package/src/lib/utils/dom.ts +5 -3
- package/src/lib/utils/getPointerInfo.ts +2 -1
- package/src/lib/utils/pointer.test.ts +48 -0
- package/src/lib/utils/pointer.ts +18 -0
- package/src/version.ts +3 -3
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
- package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
|
@@ -48,7 +48,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
48
48
|
switch (this.state) {
|
|
49
49
|
case HistoryRecorderState.Recording:
|
|
50
50
|
this.pendingDiff.apply(entry.changes)
|
|
51
|
-
this
|
|
51
|
+
// this interceptor runs for every store change, so skip the update when the
|
|
52
|
+
// redo stack is already empty
|
|
53
|
+
if (this.stacks.get().redos.length > 0) {
|
|
54
|
+
this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
|
|
55
|
+
}
|
|
52
56
|
break
|
|
53
57
|
case HistoryRecorderState.RecordingPreserveRedoStack:
|
|
54
58
|
this.pendingDiff.apply(entry.changes)
|
|
@@ -79,6 +83,14 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
79
83
|
return this.stacks.get().redos.length
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
/** @internal */
|
|
87
|
+
private _isReplaying = false
|
|
88
|
+
|
|
89
|
+
/** @internal */
|
|
90
|
+
isReplaying() {
|
|
91
|
+
return this._isReplaying
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/** @internal */
|
|
83
95
|
_isInBatch = false
|
|
84
96
|
|
|
@@ -115,7 +127,9 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
115
127
|
// History
|
|
116
128
|
_undo({ pushToRedoStack, toMark = undefined }: { pushToRedoStack: boolean; toMark?: string }) {
|
|
117
129
|
const previousState = this.state
|
|
130
|
+
const previousIsReplaying = this._isReplaying
|
|
118
131
|
this.state = HistoryRecorderState.Paused
|
|
132
|
+
this._isReplaying = true
|
|
119
133
|
try {
|
|
120
134
|
let { undos, redos } = this.stacks.get()
|
|
121
135
|
|
|
@@ -176,11 +190,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
176
190
|
this.pendingDiff.restore(pendingDiff)
|
|
177
191
|
return this
|
|
178
192
|
}
|
|
179
|
-
|
|
180
193
|
this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true })
|
|
181
194
|
this.store.ensureStoreIsUsable()
|
|
182
195
|
this.stacks.set({ undos, redos })
|
|
183
196
|
} finally {
|
|
197
|
+
this._isReplaying = previousIsReplaying
|
|
184
198
|
this.state = previousState
|
|
185
199
|
}
|
|
186
200
|
|
|
@@ -195,7 +209,9 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
195
209
|
|
|
196
210
|
redo() {
|
|
197
211
|
const previousState = this.state
|
|
212
|
+
const previousIsReplaying = this._isReplaying
|
|
198
213
|
this.state = HistoryRecorderState.Paused
|
|
214
|
+
this._isReplaying = true
|
|
199
215
|
try {
|
|
200
216
|
this.flushPendingDiff()
|
|
201
217
|
|
|
@@ -224,11 +240,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
|
|
224
240
|
break
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
|
-
|
|
228
243
|
this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true })
|
|
229
244
|
this.store.ensureStoreIsUsable()
|
|
230
245
|
this.stacks.set({ undos, redos })
|
|
231
246
|
} finally {
|
|
247
|
+
this._isReplaying = previousIsReplaying
|
|
232
248
|
this.state = previousState
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -349,7 +365,16 @@ class PendingDiff<R extends UnknownRecord> {
|
|
|
349
365
|
|
|
350
366
|
apply(diff: RecordsDiff<R>) {
|
|
351
367
|
squashRecordDiffsMutable(this.diff, [diff])
|
|
352
|
-
|
|
368
|
+
// Recomputing emptiness from the accumulated diff is O(N) (for-in over a
|
|
369
|
+
// dictionary-mode object pays an O(N) key-collection prologue), and this runs on
|
|
370
|
+
// every history interceptor call — e.g. every input tick while resizing many
|
|
371
|
+
// shapes. Updates can never cancel out existing entries during a squash, so the
|
|
372
|
+
// full recompute is only needed when the incoming diff adds or removes records.
|
|
373
|
+
if (hasAnyKey(diff.added) || hasAnyKey(diff.removed)) {
|
|
374
|
+
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
|
|
375
|
+
} else if (this.isEmptyAtom.__unsafe__getWithoutCapture()) {
|
|
376
|
+
this.isEmptyAtom.set(!hasAnyKey(diff.updated))
|
|
377
|
+
}
|
|
353
378
|
}
|
|
354
379
|
|
|
355
380
|
debug() {
|
|
@@ -357,6 +382,11 @@ class PendingDiff<R extends UnknownRecord> {
|
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
384
|
|
|
385
|
+
function hasAnyKey(obj: object) {
|
|
386
|
+
for (const _ in obj) return true
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
|
|
360
390
|
type Stack<T> = StackItem<T> | EmptyStackItem<T>
|
|
361
391
|
|
|
362
392
|
function stack<T>(): Stack<T> {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createTLStore } from '../../../config/createTLStore'
|
|
2
|
+
import { Editor } from '../../Editor'
|
|
3
|
+
|
|
4
|
+
function createTestEditor() {
|
|
5
|
+
const store = createTLStore({})
|
|
6
|
+
store.ensureStoreIsUsable()
|
|
7
|
+
return new Editor({
|
|
8
|
+
store,
|
|
9
|
+
bindingUtils: [],
|
|
10
|
+
shapeUtils: [],
|
|
11
|
+
getContainer: () => document.createElement('div'),
|
|
12
|
+
tools: [],
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('InputsManager', () => {
|
|
17
|
+
let editor: Editor
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
editor = createTestEditor()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
editor.dispose()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('updates pointer velocity on frame events', () => {
|
|
28
|
+
const point = editor.inputs.getCurrentScreenPoint()
|
|
29
|
+
point.x = 0
|
|
30
|
+
point.y = 0
|
|
31
|
+
editor.emit('frame', 16)
|
|
32
|
+
|
|
33
|
+
point.x = 100
|
|
34
|
+
point.y = 0
|
|
35
|
+
editor.emit('frame', 16)
|
|
36
|
+
|
|
37
|
+
expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('stops updating pointer velocity after dispose', () => {
|
|
41
|
+
const point = editor.inputs.getCurrentScreenPoint()
|
|
42
|
+
point.x = 0
|
|
43
|
+
point.y = 0
|
|
44
|
+
editor.emit('frame', 16)
|
|
45
|
+
|
|
46
|
+
point.x = 100
|
|
47
|
+
point.y = 0
|
|
48
|
+
editor.emit('frame', 16)
|
|
49
|
+
|
|
50
|
+
const velocityBeforeDispose = editor.inputs.getPointerVelocity().clone()
|
|
51
|
+
expect(velocityBeforeDispose.len()).toBeGreaterThan(0)
|
|
52
|
+
|
|
53
|
+
editor.inputs.dispose()
|
|
54
|
+
|
|
55
|
+
point.x = 200
|
|
56
|
+
point.y = 0
|
|
57
|
+
editor.emit('frame', 16)
|
|
58
|
+
|
|
59
|
+
expect(editor.inputs.getPointerVelocity()).toEqual(velocityBeforeDispose)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { atom, computed, unsafe__withoutCapture } from '@tldraw/state'
|
|
2
2
|
import { AtomSet } from '@tldraw/store'
|
|
3
3
|
import { TLINSTANCE_ID, TLPOINTER_ID } from '@tldraw/tlschema'
|
|
4
|
+
import { bind } from '@tldraw/utils'
|
|
4
5
|
import { INTERNAL_POINTER_IDS } from '../../../constants'
|
|
5
6
|
import { Vec } from '../../../primitives/Vec'
|
|
6
7
|
import { isAccelKey } from '../../../utils/keyboard'
|
|
@@ -12,7 +13,19 @@ const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
|
|
|
12
13
|
|
|
13
14
|
/** @public */
|
|
14
15
|
export class InputsManager {
|
|
15
|
-
constructor(private readonly editor: Editor) {
|
|
16
|
+
constructor(private readonly editor: Editor) {
|
|
17
|
+
this.editor.on('frame', this._onFrame)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal */
|
|
21
|
+
dispose() {
|
|
22
|
+
this.editor.off('frame', this._onFrame)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@bind
|
|
26
|
+
private _onFrame(elapsed: number) {
|
|
27
|
+
this.updatePointerVelocity(elapsed)
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
private _originPagePoint = atom<Vec>('originPagePoint', new Vec())
|
|
18
31
|
/**
|
|
@@ -120,8 +133,7 @@ export class InputsManager {
|
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
/**
|
|
123
|
-
* Normally you shouldn't need to set the pointer velocity directly
|
|
124
|
-
* However, this is currently used in tests to fake pointer velocity.
|
|
136
|
+
* Normally you shouldn't need to set the pointer velocity directly. Used in tests to fake pointer velocity.
|
|
125
137
|
* @param pointerVelocity - The pointer velocity.
|
|
126
138
|
* @internal
|
|
127
139
|
*/
|
|
@@ -460,7 +472,7 @@ export class InputsManager {
|
|
|
460
472
|
private _velocityPrevPoint = new Vec()
|
|
461
473
|
|
|
462
474
|
/**
|
|
463
|
-
* Update the pointer velocity based on elapsed time. Called
|
|
475
|
+
* Update the pointer velocity based on elapsed time. Called each frame.
|
|
464
476
|
* @param elapsed - The time elapsed since the last tick in milliseconds.
|
|
465
477
|
* @internal
|
|
466
478
|
*/
|
|
@@ -46,6 +46,10 @@ export class SpatialIndexManager {
|
|
|
46
46
|
|
|
47
47
|
private createSpatialIndexComputed() {
|
|
48
48
|
const shapeHistory = this.editor.store.query.filterHistory('shape')
|
|
49
|
+
// Binding changes can move a shape's derived bounds (e.g. creating or
|
|
50
|
+
// deleting an arrow binding relocates the arrow's body) without
|
|
51
|
+
// touching any shape record, so they must also invalidate the index.
|
|
52
|
+
const bindingHistory = this.editor.store.query.filterHistory('binding')
|
|
49
53
|
|
|
50
54
|
return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
|
|
51
55
|
if (isUninitialized(_prevValue)) {
|
|
@@ -53,8 +57,9 @@ export class SpatialIndexManager {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
60
|
+
const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)
|
|
56
61
|
|
|
57
|
-
if (shapeDiff === RESET_VALUE) {
|
|
62
|
+
if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
|
|
58
63
|
return this.rebuildAndBumpEpoch()
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -63,8 +68,10 @@ export class SpatialIndexManager {
|
|
|
63
68
|
return this.rebuildAndBumpEpoch()
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
if (shapeDiff.length === 0) return this._boundsEpoch
|
|
71
|
+
if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch
|
|
67
72
|
|
|
73
|
+
// A binding-only diff passes an empty shape diff: step 1 is a no-op
|
|
74
|
+
// and the step-2 sweep re-checks the indexed bounds of every shape.
|
|
68
75
|
if (this.processIncrementalUpdate(shapeDiff)) {
|
|
69
76
|
this._boundsEpoch++
|
|
70
77
|
}
|
|
@@ -82,20 +82,22 @@ const mockEditor = {
|
|
|
82
82
|
getContainerDocument: vi.fn(() => mockDocument),
|
|
83
83
|
} as unknown as Editor
|
|
84
84
|
|
|
85
|
-
global.Range = vi.fn(()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
global.Range = vi.fn(function () {
|
|
86
|
+
return {
|
|
87
|
+
setStart: vi.fn(),
|
|
88
|
+
setEnd: vi.fn(),
|
|
89
|
+
getClientRects: vi.fn(() => [
|
|
90
|
+
{
|
|
91
|
+
width: 10,
|
|
92
|
+
height: 16,
|
|
93
|
+
left: 0,
|
|
94
|
+
top: 0,
|
|
95
|
+
right: 10,
|
|
96
|
+
bottom: 16,
|
|
97
|
+
},
|
|
98
|
+
]),
|
|
99
|
+
}
|
|
100
|
+
}) as any
|
|
99
101
|
|
|
100
102
|
describe('TextManager', () => {
|
|
101
103
|
let textManager: TextManager
|
|
@@ -2,6 +2,21 @@ import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
|
2
2
|
import { objectMapKeys } from '@tldraw/utils'
|
|
3
3
|
import type { Editor } from '../../Editor'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The whole-pixel line-height for a given font size and tldraw's unitless line-height
|
|
7
|
+
* multiplier. tldraw's theme stores line-height as a multiplier (e.g. 1.35); resolving it
|
|
8
|
+
* to a whole pixel keeps line spacing identical across rendering engines, which otherwise
|
|
9
|
+
* disagree on fractional line boxes (WebKit snaps them to whole pixels, Blink keeps the
|
|
10
|
+
* fraction) and let multi-line text drift apart. Apply it everywhere line-height is used —
|
|
11
|
+
* measurement, on-canvas render, and export — so geometry and rendering agree.
|
|
12
|
+
* See https://github.com/tldraw/tldraw/issues/8970.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export function resolveLineHeightPx(fontSize: number, lineHeight: number): number {
|
|
17
|
+
return Math.round(fontSize * lineHeight)
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
const fixNewLines = /\r?\n|\r/g
|
|
6
21
|
|
|
7
22
|
function normalizeTextForDom(text: string) {
|
|
@@ -154,7 +169,7 @@ export class TextManager {
|
|
|
154
169
|
'font-style': opts.fontStyle,
|
|
155
170
|
'font-weight': opts.fontWeight,
|
|
156
171
|
'font-size': opts.fontSize + 'px',
|
|
157
|
-
'line-height': opts.lineHeight
|
|
172
|
+
'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
|
|
158
173
|
padding: opts.padding,
|
|
159
174
|
'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
|
|
160
175
|
'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
|
|
@@ -382,7 +397,7 @@ export class TextManager {
|
|
|
382
397
|
'font-style': opts.fontStyle,
|
|
383
398
|
'font-weight': opts.fontWeight,
|
|
384
399
|
'font-size': opts.fontSize + 'px',
|
|
385
|
-
'line-height': opts.lineHeight
|
|
400
|
+
'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
|
|
386
401
|
width: `${elementWidth}px`,
|
|
387
402
|
height: 'min-content',
|
|
388
403
|
'text-align': textAlignmentsForLtr[opts.textAlign],
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Mock, Mocked, vi } from 'vitest'
|
|
2
|
-
import { Vec } from '../../../primitives/Vec'
|
|
3
2
|
import { Editor } from '../../Editor'
|
|
4
3
|
import { TickManager } from './TickManager'
|
|
5
4
|
|
|
@@ -21,17 +20,6 @@ describe('TickManager', () => {
|
|
|
21
20
|
let tickManager: TickManager
|
|
22
21
|
let mockEmit: Mock
|
|
23
22
|
let mockDisposablesAdd: Mock
|
|
24
|
-
let mockInputs: {
|
|
25
|
-
_currentScreenPoint: Vec
|
|
26
|
-
getCurrentScreenPoint(): Vec
|
|
27
|
-
currentScreenPoint: Vec
|
|
28
|
-
setCurrentScreenPoint(value: Vec): void
|
|
29
|
-
_pointerVelocity: Vec
|
|
30
|
-
getPointerVelocity(): Vec
|
|
31
|
-
pointerVelocity: Vec
|
|
32
|
-
setPointerVelocity(value: Vec): void
|
|
33
|
-
updatePointerVelocity(elapsed: number): void
|
|
34
|
-
}
|
|
35
23
|
|
|
36
24
|
beforeEach(() => {
|
|
37
25
|
vi.clearAllMocks()
|
|
@@ -52,39 +40,11 @@ describe('TickManager', () => {
|
|
|
52
40
|
mockEmit = vi.fn()
|
|
53
41
|
mockDisposablesAdd = vi.fn()
|
|
54
42
|
|
|
55
|
-
// Create a mock inputs object with getters and setters
|
|
56
|
-
mockInputs = {
|
|
57
|
-
_currentScreenPoint: new Vec(100, 100),
|
|
58
|
-
getCurrentScreenPoint() {
|
|
59
|
-
return this._currentScreenPoint
|
|
60
|
-
},
|
|
61
|
-
get currentScreenPoint() {
|
|
62
|
-
return this.getCurrentScreenPoint()
|
|
63
|
-
},
|
|
64
|
-
setCurrentScreenPoint(value: Vec) {
|
|
65
|
-
this._currentScreenPoint = value
|
|
66
|
-
},
|
|
67
|
-
_pointerVelocity: new Vec(0, 0),
|
|
68
|
-
getPointerVelocity() {
|
|
69
|
-
return this._pointerVelocity
|
|
70
|
-
},
|
|
71
|
-
get pointerVelocity() {
|
|
72
|
-
return this.getPointerVelocity()
|
|
73
|
-
},
|
|
74
|
-
setPointerVelocity(value: Vec) {
|
|
75
|
-
this._pointerVelocity = value
|
|
76
|
-
},
|
|
77
|
-
updatePointerVelocity(_elapsed: number) {
|
|
78
|
-
// Mock implementation - no-op for tests
|
|
79
|
-
},
|
|
80
|
-
}
|
|
81
|
-
|
|
82
43
|
editor = {
|
|
83
44
|
emit: mockEmit,
|
|
84
45
|
disposables: {
|
|
85
46
|
add: mockDisposablesAdd,
|
|
86
47
|
},
|
|
87
|
-
inputs: mockInputs as unknown as Editor['inputs'],
|
|
88
48
|
} as unknown as Mocked<Editor>
|
|
89
49
|
|
|
90
50
|
tickManager = new TickManager(editor)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom } from '@tldraw/state'
|
|
2
|
+
import { createUserId } from '@tldraw/tlschema'
|
|
2
3
|
import { Mocked, vi } from 'vitest'
|
|
3
4
|
import { TLCurrentUser } from '../../../config/createTLCurrentUser'
|
|
4
5
|
import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
|
|
@@ -296,8 +297,17 @@ describe('UserPreferencesManager', () => {
|
|
|
296
297
|
userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
|
|
297
298
|
})
|
|
298
299
|
|
|
299
|
-
describe('
|
|
300
|
-
it('should return user id', () => {
|
|
300
|
+
describe('getExternalId / getRecordId', () => {
|
|
301
|
+
it('should return the raw external user id', () => {
|
|
302
|
+
expect(userPreferencesManager.getExternalId()).toBe(mockUserPreferences.id)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should return the prefixed record id', () => {
|
|
306
|
+
expect(userPreferencesManager.getRecordId()).toBe(createUserId(mockUserPreferences.id))
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('getId() still returns the external id', () => {
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
301
311
|
expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
|
|
302
312
|
})
|
|
303
313
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom, computed } from '@tldraw/state'
|
|
2
|
+
import { createUserId, TLUserId } from '@tldraw/tlschema'
|
|
2
3
|
import { TLCurrentUser } from '../../../config/createTLCurrentUser'
|
|
3
4
|
import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
|
|
4
5
|
import { getGlobalWindow } from '../../../utils/dom'
|
|
@@ -39,7 +40,7 @@ export class UserPreferencesManager {
|
|
|
39
40
|
}
|
|
40
41
|
@computed getUserPreferences() {
|
|
41
42
|
return {
|
|
42
|
-
id: this.
|
|
43
|
+
id: this.getExternalId(),
|
|
43
44
|
name: this.getName(),
|
|
44
45
|
locale: this.getLocale(),
|
|
45
46
|
color: this.getColor(),
|
|
@@ -89,10 +90,34 @@ export class UserPreferencesManager {
|
|
|
89
90
|
)
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The current user's raw, app-provided id — the value set in the user's
|
|
95
|
+
* {@link @tldraw/editor#TLUserPreferences}. Use this when you need the id your application
|
|
96
|
+
* assigned to the user. To compare against or look up store records, use
|
|
97
|
+
* {@link UserPreferencesManager.getRecordId} instead.
|
|
98
|
+
*/
|
|
99
|
+
@computed getExternalId(): string {
|
|
93
100
|
return this.user.userPreferences.get().id
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Use {@link UserPreferencesManager.getExternalId} for the raw app-provided id, or
|
|
105
|
+
* {@link UserPreferencesManager.getRecordId} for the prefixed `TLUserId` record id.
|
|
106
|
+
*/
|
|
107
|
+
@computed getId() {
|
|
108
|
+
return this.getExternalId()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The current user's id as a tldraw {@link @tldraw/tlschema#TLUserId} record id (prefixed
|
|
113
|
+
* with `user:`). Use this when comparing against or looking up store records, such as a
|
|
114
|
+
* presence record's `userId` or `followingUserId`. For the raw, app-provided id, use
|
|
115
|
+
* {@link UserPreferencesManager.getExternalId}.
|
|
116
|
+
*/
|
|
117
|
+
@computed getRecordId(): TLUserId {
|
|
118
|
+
return createUserId(this.getExternalId())
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
@computed getName() {
|
|
97
122
|
return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
|
|
98
123
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createComputedCache } from '@tldraw/store'
|
|
2
|
+
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
3
|
+
import type { Editor } from '../Editor'
|
|
4
|
+
|
|
5
|
+
const indicatorPathCache = createComputedCache(
|
|
6
|
+
'shapeIndicatorPath',
|
|
7
|
+
(editor: Editor, shape: TLShape) => {
|
|
8
|
+
const util = editor.getShapeUtil(shape)
|
|
9
|
+
return util.getIndicatorPath(shape)
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
areRecordsEqual(a, b) {
|
|
13
|
+
return a.props === b.props
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Combine every batchable shape indicator into a single page-space `Path2D` and
|
|
20
|
+
* emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
|
|
21
|
+
* arrows with labels or complex arrowheads) can't be batched — they still
|
|
22
|
+
* stroke individually inside a save/restore with `ctx.clip` applied.
|
|
23
|
+
*
|
|
24
|
+
* Shared by any overlay util that paints shape indicators (e.g. collaborator
|
|
25
|
+
* selections).
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function strokeShapeIndicators(
|
|
30
|
+
editor: Editor,
|
|
31
|
+
ctx: CanvasRenderingContext2D,
|
|
32
|
+
shapeIds: TLShapeId[]
|
|
33
|
+
): void {
|
|
34
|
+
if (shapeIds.length === 0) return
|
|
35
|
+
|
|
36
|
+
const batched = new Path2D()
|
|
37
|
+
|
|
38
|
+
for (const shapeId of shapeIds) {
|
|
39
|
+
const shape = editor.getShape(shapeId)
|
|
40
|
+
if (!shape || shape.isLocked) continue
|
|
41
|
+
|
|
42
|
+
const pageTransform = editor.getShapePageTransform(shape)
|
|
43
|
+
if (!pageTransform) continue
|
|
44
|
+
|
|
45
|
+
const indicatorPath = indicatorPathCache.get(editor, shape.id)
|
|
46
|
+
if (!indicatorPath) continue
|
|
47
|
+
|
|
48
|
+
if (indicatorPath instanceof Path2D) {
|
|
49
|
+
batched.addPath(indicatorPath, pageTransform)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { path, clipPath, additionalPaths } = indicatorPath
|
|
54
|
+
|
|
55
|
+
if (!clipPath) {
|
|
56
|
+
batched.addPath(path, pageTransform)
|
|
57
|
+
if (additionalPaths) {
|
|
58
|
+
for (const p of additionalPaths) batched.addPath(p, pageTransform)
|
|
59
|
+
}
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Clipped case: fall back to an individual stroke. Rare (arrows with
|
|
64
|
+
// labels / complex arrowheads), so the extra save/restore/stroke
|
|
65
|
+
// pair per such shape isn't worth batching away.
|
|
66
|
+
ctx.save()
|
|
67
|
+
ctx.transform(
|
|
68
|
+
pageTransform.a,
|
|
69
|
+
pageTransform.b,
|
|
70
|
+
pageTransform.c,
|
|
71
|
+
pageTransform.d,
|
|
72
|
+
pageTransform.e,
|
|
73
|
+
pageTransform.f
|
|
74
|
+
)
|
|
75
|
+
ctx.save()
|
|
76
|
+
ctx.clip(clipPath, 'evenodd')
|
|
77
|
+
ctx.stroke(path)
|
|
78
|
+
ctx.restore()
|
|
79
|
+
if (additionalPaths) {
|
|
80
|
+
for (const p of additionalPaths) ctx.stroke(p)
|
|
81
|
+
}
|
|
82
|
+
ctx.restore()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ctx.stroke(batched)
|
|
86
|
+
}
|
|
@@ -77,6 +77,10 @@ export class Pointing extends StateNode {
|
|
|
77
77
|
this.cancel()
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
override onLongPress() {
|
|
81
|
+
if (this.editor.getInstanceState().isCoarsePointer) this.cancel()
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
complete() {
|
|
81
85
|
const originPagePoint = this.editor.inputs.getOriginPagePoint()
|
|
82
86
|
|
|
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
268
268
|
onLongPress?(info: TLPointerEventInfo): void
|
|
269
269
|
onPointerUp?(info: TLPointerEventInfo): void
|
|
270
270
|
onDoubleClick?(info: TLClickEventInfo): void
|
|
271
|
-
onTripleClick?(info: TLClickEventInfo): void
|
|
272
|
-
onQuadrupleClick?(info: TLClickEventInfo): void
|
|
273
271
|
onRightClick?(info: TLPointerEventInfo): void
|
|
274
272
|
onMiddleClick?(info: TLPointerEventInfo): void
|
|
275
273
|
onKeyDown?(info: TLKeyboardEventInfo): void
|
|
@@ -24,7 +24,7 @@ export type TLPointerEventName =
|
|
|
24
24
|
| 'middle_click'
|
|
25
25
|
|
|
26
26
|
/** @public */
|
|
27
|
-
export type TLCLickEventName = 'double_click'
|
|
27
|
+
export type TLCLickEventName = 'double_click'
|
|
28
28
|
|
|
29
29
|
/** @public */
|
|
30
30
|
export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
|
|
@@ -72,7 +72,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
|
|
|
72
72
|
point: VecLike
|
|
73
73
|
pointerId: number
|
|
74
74
|
button: number
|
|
75
|
-
phase: 'down' | 'up' | 'settle'
|
|
75
|
+
phase: 'down' | 'up' | 'settle-down' | 'settle-up'
|
|
76
76
|
} & TLPointerEventTarget
|
|
77
77
|
|
|
78
78
|
/** @public */
|
|
@@ -173,8 +173,6 @@ export interface TLEventHandlers {
|
|
|
173
173
|
onLongPress: TLPointerEvent
|
|
174
174
|
onRightClick: TLPointerEvent
|
|
175
175
|
onDoubleClick: TLClickEvent
|
|
176
|
-
onTripleClick: TLClickEvent
|
|
177
|
-
onQuadrupleClick: TLClickEvent
|
|
178
176
|
onMiddleClick: TLPointerEvent
|
|
179
177
|
onPointerUp: TLPointerEvent
|
|
180
178
|
onKeyDown: TLKeyboardEvent
|
|
@@ -206,7 +204,5 @@ export const EVENT_NAME_MAP: Record<
|
|
|
206
204
|
complete: 'onComplete',
|
|
207
205
|
interrupt: 'onInterrupt',
|
|
208
206
|
double_click: 'onDoubleClick',
|
|
209
|
-
triple_click: 'onTripleClick',
|
|
210
|
-
quadruple_click: 'onQuadrupleClick',
|
|
211
207
|
tick: 'onTick',
|
|
212
208
|
}
|