@tldraw/editor 5.4.0-next.09eca5dee30d → 5.4.0-next.0a83c4ec44f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +114 -47
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +7 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +181 -294
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/globals/editors.js +57 -0
- package/dist-cjs/lib/globals/editors.js.map +7 -0
- package/dist-cjs/lib/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +9 -2
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +16 -3
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +5 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
- package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/lib/utils/sync/alerts.js +1 -1
- package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +114 -47
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +9 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +189 -295
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/globals/editors.mjs +37 -0
- package/dist-esm/lib/globals/editors.mjs.map +7 -0
- package/dist-esm/lib/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +9 -2
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +5 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
- package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
- package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +13 -7
- package/src/index.ts +6 -1
- package/src/lib/TldrawEditor.tsx +16 -4
- package/src/lib/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +266 -383
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/editors.test.ts +76 -0
- package/src/lib/globals/editors.ts +53 -0
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +60 -16
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -1
- package/src/lib/license/LicenseManager.test.ts +160 -0
- package/src/lib/license/LicenseManager.ts +13 -4
- package/src/lib/license/LicenseProvider.tsx +24 -3
- package/src/lib/license/useLicenseManagerState.ts +8 -6
- package/src/lib/primitives/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
- package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
- package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
- package/src/lib/primitives/geometry/Group2d.ts +63 -27
- package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
- package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +107 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +7 -1
- package/src/lib/utils/normalizeWheel.test.ts +39 -0
- package/src/lib/utils/normalizeWheel.ts +6 -0
- package/src/lib/utils/rotation.ts +4 -12
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
- package/src/lib/utils/sync/alerts.ts +1 -1
- package/src/version.ts +3 -3
- package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
|
@@ -39,9 +39,13 @@ export function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: ()
|
|
|
39
39
|
function fromScratch() {
|
|
40
40
|
const currentPageId = getCurrentPageId()
|
|
41
41
|
lastPageId = currentPageId
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const result = new Set<TLShapeId>()
|
|
43
|
+
for (const id of shapesIndex.get()) {
|
|
44
|
+
if (isShapeInPage(store, currentPageId, store.get(id)!)) {
|
|
45
|
+
result.add(id)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
45
49
|
}
|
|
46
50
|
return computed<Set<TLShapeId>>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => {
|
|
47
51
|
if (isUninitialized(prevValue)) {
|
|
@@ -94,7 +94,7 @@ export class ClickManager {
|
|
|
94
94
|
switch (this._clickState) {
|
|
95
95
|
case 'pendingDouble': {
|
|
96
96
|
this._clickState = 'pendingOverflow'
|
|
97
|
-
this.
|
|
97
|
+
this._getClickTimeout(this._clickState)
|
|
98
98
|
return {
|
|
99
99
|
...info,
|
|
100
100
|
type: 'click',
|
|
@@ -114,7 +114,7 @@ export class ClickManager {
|
|
|
114
114
|
// overflow
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
this.
|
|
117
|
+
this._getClickTimeout(this._clickState)
|
|
118
118
|
return info
|
|
119
119
|
}
|
|
120
120
|
case 'pointer_up': {
|
|
@@ -141,10 +141,12 @@ export class ClickManager {
|
|
|
141
141
|
return info
|
|
142
142
|
}
|
|
143
143
|
case 'pointer_move': {
|
|
144
|
+
// Compare in client coordinates like _clickScreenPoint; the container-relative
|
|
145
|
+
// inputs screen point would add the container offset and cancel on any movement.
|
|
144
146
|
if (
|
|
145
147
|
this._clickState !== 'idle' &&
|
|
146
148
|
this._clickScreenPoint &&
|
|
147
|
-
Vec.Dist2(this._clickScreenPoint,
|
|
149
|
+
Vec.Dist2(this._clickScreenPoint, info.point) >
|
|
148
150
|
(this.editor.getInstanceState().isCoarsePointer
|
|
149
151
|
? this.editor.options.coarseDragDistanceSquared
|
|
150
152
|
: this.editor.options.dragDistanceSquared)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
|
|
2
2
|
import type { TLInstancePresence } from '@tldraw/tlschema'
|
|
3
|
-
import { areArraysShallowEqual
|
|
3
|
+
import { areArraysShallowEqual } from '@tldraw/utils'
|
|
4
4
|
import type { Editor } from '../../Editor'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -54,14 +54,14 @@ export class CollaboratorsManager {
|
|
|
54
54
|
getCollaborators(): TLInstancePresence[] {
|
|
55
55
|
const allPresenceRecords = this._getCollaboratorsQuery().get()
|
|
56
56
|
if (!allPresenceRecords.length) return EMPTY_ARRAY
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const latestByUserId = new Map<string, TLInstancePresence>()
|
|
58
|
+
for (const presence of allPresenceRecords) {
|
|
59
|
+
const latest = latestByUserId.get(presence.userId)
|
|
60
|
+
if (!latest || (presence.lastActivityTimestamp ?? 0) > (latest.lastActivityTimestamp ?? 0)) {
|
|
61
|
+
latestByUserId.set(presence.userId, presence)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...latestByUserId.keys()].sort().map((id) => latestByUserId.get(id)!)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
mapObjectMapValues,
|
|
9
9
|
objectMapEntries,
|
|
10
10
|
} from '@tldraw/utils'
|
|
11
|
+
import { areShapesContentEqual } from '../../../utils/areShapesContentEqual'
|
|
11
12
|
import type { Editor } from '../../Editor'
|
|
12
13
|
|
|
13
14
|
interface FontState {
|
|
@@ -41,7 +42,7 @@ export class FontManager {
|
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
areResultsEqual: areArraysShallowEqual,
|
|
44
|
-
areRecordsEqual:
|
|
45
|
+
areRecordsEqual: areShapesContentEqual,
|
|
45
46
|
}
|
|
46
47
|
)
|
|
47
48
|
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
import { InstancePresenceRecordType, TLPOINTER_ID, createUserId } from '@tldraw/tlschema'
|
|
2
|
+
import { vi } from 'vitest'
|
|
1
3
|
import { createTLStore } from '../../../config/createTLStore'
|
|
4
|
+
import { TldrawOptions } from '../../../options'
|
|
2
5
|
import { Editor } from '../../Editor'
|
|
6
|
+
import { StateNode } from '../../tools/StateNode'
|
|
3
7
|
|
|
4
|
-
function
|
|
8
|
+
function createPresence(editor: Editor) {
|
|
9
|
+
return InstancePresenceRecordType.create({
|
|
10
|
+
id: InstancePresenceRecordType.createId('peer'),
|
|
11
|
+
userId: createUserId('peer'),
|
|
12
|
+
userName: 'Peer',
|
|
13
|
+
currentPageId: editor.getCurrentPageId(),
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createTestEditor(options?: Partial<TldrawOptions>) {
|
|
5
18
|
const store = createTLStore({})
|
|
6
19
|
store.ensureStoreIsUsable()
|
|
7
|
-
|
|
20
|
+
// Attached to the document so that events dispatched on the container
|
|
21
|
+
// propagate to the window, where the activity listeners live.
|
|
22
|
+
const container = document.createElement('div')
|
|
23
|
+
document.body.appendChild(container)
|
|
24
|
+
const editor = new Editor({
|
|
8
25
|
store,
|
|
9
26
|
bindingUtils: [],
|
|
10
27
|
shapeUtils: [],
|
|
11
|
-
getContainer: () =>
|
|
28
|
+
getContainer: () => container,
|
|
12
29
|
tools: [],
|
|
30
|
+
options,
|
|
13
31
|
})
|
|
32
|
+
editor.disposables.add(() => container.remove())
|
|
33
|
+
// Input listeners only attach once the editor is mounted, which is normally
|
|
34
|
+
// emitted by `TldrawEditor` when the container is in the DOM.
|
|
35
|
+
editor.emit('mount')
|
|
36
|
+
return editor
|
|
14
37
|
}
|
|
15
38
|
|
|
16
39
|
describe('InputsManager', () => {
|
|
@@ -37,6 +60,179 @@ describe('InputsManager', () => {
|
|
|
37
60
|
expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
|
|
38
61
|
})
|
|
39
62
|
|
|
63
|
+
describe('markActivity', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
vi.useFakeTimers()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.useRealTimers()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('does nothing when there are no collaborators', () => {
|
|
73
|
+
editor.inputs.markActivity()
|
|
74
|
+
|
|
75
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(0)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('stamps the pointer record, preserving its position', () => {
|
|
79
|
+
editor.store.put([createPresence(editor)])
|
|
80
|
+
editor.store.put([{ ...editor.store.get(TLPOINTER_ID)!, x: 5, y: 6 }])
|
|
81
|
+
|
|
82
|
+
editor.inputs.markActivity()
|
|
83
|
+
|
|
84
|
+
expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
|
|
85
|
+
x: 5,
|
|
86
|
+
y: 6,
|
|
87
|
+
lastActivityTimestamp: Date.now(),
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('throttles stamps on the leading edge', () => {
|
|
92
|
+
editor.store.put([createPresence(editor)])
|
|
93
|
+
|
|
94
|
+
editor.inputs.markActivity()
|
|
95
|
+
const firstStamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
96
|
+
|
|
97
|
+
vi.advanceTimersByTime(500)
|
|
98
|
+
editor.inputs.markActivity()
|
|
99
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp)
|
|
100
|
+
|
|
101
|
+
vi.advanceTimersByTime(600)
|
|
102
|
+
editor.inputs.markActivity()
|
|
103
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp + 1100)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('stamps on keyboard input in the container', () => {
|
|
107
|
+
editor.store.put([createPresence(editor)])
|
|
108
|
+
|
|
109
|
+
editor.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
|
|
110
|
+
|
|
111
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('stamps on pointer input in the container', () => {
|
|
115
|
+
editor.store.put([createPresence(editor)])
|
|
116
|
+
|
|
117
|
+
editor.getContainer().dispatchEvent(new Event('pointermove', { bubbles: true }))
|
|
118
|
+
|
|
119
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('stamps on text input that fires no keydown, e.g. IME and gesture typing', () => {
|
|
123
|
+
editor.store.put([createPresence(editor)])
|
|
124
|
+
|
|
125
|
+
editor.getContainer().dispatchEvent(new Event('beforeinput', { bubbles: true }))
|
|
126
|
+
|
|
127
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('stamps on input elsewhere in the tab, outside the container', () => {
|
|
131
|
+
editor.store.put([createPresence(editor)])
|
|
132
|
+
|
|
133
|
+
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
|
|
134
|
+
|
|
135
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('keeps a minimum throttle window when the idle timeout is tiny', () => {
|
|
139
|
+
const tinyEditor = createTestEditor({ collaboratorIdleTimeoutMs: 0 })
|
|
140
|
+
try {
|
|
141
|
+
tinyEditor.store.put([createPresence(tinyEditor)])
|
|
142
|
+
|
|
143
|
+
tinyEditor.inputs.markActivity()
|
|
144
|
+
const stamp = tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
145
|
+
|
|
146
|
+
vi.advanceTimersByTime(50)
|
|
147
|
+
tinyEditor.inputs.markActivity()
|
|
148
|
+
expect(tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(stamp)
|
|
149
|
+
} finally {
|
|
150
|
+
tinyEditor.dispose()
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('attaches input listeners only while sharing with someone', () => {
|
|
155
|
+
const addEventListener = vi.spyOn(window, 'addEventListener')
|
|
156
|
+
const removeEventListener = vi.spyOn(window, 'removeEventListener')
|
|
157
|
+
const soloEditor = createTestEditor()
|
|
158
|
+
try {
|
|
159
|
+
expect(addEventListener).not.toHaveBeenCalled()
|
|
160
|
+
|
|
161
|
+
soloEditor.store.put([createPresence(soloEditor)])
|
|
162
|
+
expect(addEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
|
|
163
|
+
capture: true,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
soloEditor.store.remove([createPresence(soloEditor).id])
|
|
167
|
+
expect(removeEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
|
|
168
|
+
capture: true,
|
|
169
|
+
})
|
|
170
|
+
} finally {
|
|
171
|
+
soloEditor.dispose()
|
|
172
|
+
addEventListener.mockRestore()
|
|
173
|
+
removeEventListener.mockRestore()
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('attaches no input listeners when construction fails before mount', () => {
|
|
178
|
+
class DuplicateTool extends StateNode {
|
|
179
|
+
static override id = 'duplicate-tool'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const addEventListener = vi.spyOn(window, 'addEventListener')
|
|
183
|
+
try {
|
|
184
|
+
const store = createTLStore({})
|
|
185
|
+
store.ensureStoreIsUsable()
|
|
186
|
+
expect(
|
|
187
|
+
() =>
|
|
188
|
+
new Editor({
|
|
189
|
+
store,
|
|
190
|
+
bindingUtils: [],
|
|
191
|
+
shapeUtils: [],
|
|
192
|
+
getContainer: () => document.createElement('div'),
|
|
193
|
+
// The duplicate id makes the constructor throw after the inputs
|
|
194
|
+
// manager is built, so the editor is never disposed.
|
|
195
|
+
tools: [DuplicateTool, DuplicateTool],
|
|
196
|
+
})
|
|
197
|
+
).toThrow()
|
|
198
|
+
|
|
199
|
+
expect(addEventListener).not.toHaveBeenCalled()
|
|
200
|
+
} finally {
|
|
201
|
+
addEventListener.mockRestore()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('does not stamp on pointer moves with no DOM input, e.g. following a user', () => {
|
|
206
|
+
editor.store.put([createPresence(editor)])
|
|
207
|
+
|
|
208
|
+
editor.inputs.markActivity()
|
|
209
|
+
const stamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
210
|
+
|
|
211
|
+
vi.advanceTimersByTime(5000)
|
|
212
|
+
editor.inputs.updateFromEvent({
|
|
213
|
+
type: 'pointer',
|
|
214
|
+
name: 'pointer_move',
|
|
215
|
+
point: { x: 100, y: 50 },
|
|
216
|
+
pointerId: 1,
|
|
217
|
+
button: 0,
|
|
218
|
+
isPen: false,
|
|
219
|
+
target: 'canvas',
|
|
220
|
+
shiftKey: false,
|
|
221
|
+
altKey: false,
|
|
222
|
+
ctrlKey: false,
|
|
223
|
+
metaKey: false,
|
|
224
|
+
accelKey: false,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// The pointer position updates, but the activity clock doesn't.
|
|
228
|
+
expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
|
|
229
|
+
x: 100,
|
|
230
|
+
y: 50,
|
|
231
|
+
lastActivityTimestamp: stamp,
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
40
236
|
it('stops updating pointer velocity after dispose', () => {
|
|
41
237
|
const point = editor.inputs.getCurrentScreenPoint()
|
|
42
238
|
point.x = 0
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { atom, computed, unsafe__withoutCapture } from '@tldraw/state'
|
|
1
|
+
import { atom, computed, react, 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'
|
|
5
|
-
import { INTERNAL_POINTER_IDS } from '../../../constants'
|
|
4
|
+
import { bind, throttle } from '@tldraw/utils'
|
|
6
5
|
import { Vec } from '../../../primitives/Vec'
|
|
7
6
|
import { isAccelKey } from '../../../utils/keyboard'
|
|
8
7
|
import type { Editor } from '../../Editor'
|
|
@@ -12,11 +11,85 @@ import { EditorManager } from '../EditorManager'
|
|
|
12
11
|
const POINTER_VELOCITY_REFERENCE_INTERVAL_MS = 16
|
|
13
12
|
const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
|
|
14
13
|
|
|
14
|
+
// How often `markActivity` is allowed to stamp the activity timestamp. Each
|
|
15
|
+
// stamp re-broadcasts presence, so this must be long enough to keep input
|
|
16
|
+
// bursts (e.g. typing, held keys) from flooding the network. It must also stay
|
|
17
|
+
// comfortably shorter than `collaboratorIdleTimeoutMs` so a continuously-active
|
|
18
|
+
// peer never flickers to idle between stamps; the throttle window is capped
|
|
19
|
+
// below that option when it's configured shorter than the default, but never
|
|
20
|
+
// below the floor, so a degenerate idle timeout can't disable throttling.
|
|
21
|
+
const ACTIVITY_TIMESTAMP_THROTTLE_MS = 1000
|
|
22
|
+
const ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS = 100
|
|
23
|
+
|
|
24
|
+
// DOM events that count as presence activity. `beforeinput` covers IME and
|
|
25
|
+
// mobile keyboards (e.g. gesture typing), which often don't fire per-character
|
|
26
|
+
// keydown events. `gesturestart`/`gesturechange` are Safari's proprietary
|
|
27
|
+
// trackpad-pinch events, which produce neither pointer nor wheel events.
|
|
28
|
+
const ACTIVITY_EVENTS = [
|
|
29
|
+
'pointerdown',
|
|
30
|
+
'pointermove',
|
|
31
|
+
'pointerup',
|
|
32
|
+
'keydown',
|
|
33
|
+
'beforeinput',
|
|
34
|
+
'wheel',
|
|
35
|
+
'gesturestart',
|
|
36
|
+
'gesturechange',
|
|
37
|
+
]
|
|
38
|
+
|
|
15
39
|
/** @public */
|
|
16
40
|
export class InputsManager extends EditorManager {
|
|
17
41
|
constructor(editor: Editor) {
|
|
18
42
|
super(editor)
|
|
19
43
|
this.addEditorEvent('frame', this._onFrame)
|
|
44
|
+
|
|
45
|
+
// Track input only while mounted and sharing with someone: a solo user
|
|
46
|
+
// pays for no listeners at all, and the window is resolved on attach
|
|
47
|
+
// rather than up front, when the container is guaranteed to exist.
|
|
48
|
+
//
|
|
49
|
+
// The mount check must come first. This runs eagerly during the editor's
|
|
50
|
+
// constructor, where `editor.collaborators` isn't assigned yet, so reading
|
|
51
|
+
// collaborators here would throw; a never-mounted editor short-circuits
|
|
52
|
+
// before that, and re-runs once mount flips the atom.
|
|
53
|
+
this.register(
|
|
54
|
+
react('presence activity listeners', () => {
|
|
55
|
+
if (this.editor.getIsMounted() && this._getHasCollaborators()) {
|
|
56
|
+
this._attachActivityListeners()
|
|
57
|
+
} else {
|
|
58
|
+
this._detachActivityListeners()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
this.register(() => {
|
|
63
|
+
this._detachActivityListeners()
|
|
64
|
+
this._throttledActivityStamp.cancel()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _detachActivityListenersFn: null | (() => void) = null
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* User input anywhere in the editor's tab counts as activity for collaborator presence —
|
|
72
|
+
* presence means "this person is here", whether or not the input lands on the editor itself
|
|
73
|
+
* (e.g. typing in a sidebar next to the canvas). Listen in the capture phase so input that
|
|
74
|
+
* other handlers swallow still counts.
|
|
75
|
+
*/
|
|
76
|
+
private _attachActivityListeners() {
|
|
77
|
+
if (this._detachActivityListenersFn) return
|
|
78
|
+
|
|
79
|
+
const win = this.editor.getContainerWindow()
|
|
80
|
+
for (const name of ACTIVITY_EVENTS) {
|
|
81
|
+
win.addEventListener(name, this.markActivity, { capture: true })
|
|
82
|
+
}
|
|
83
|
+
this._detachActivityListenersFn = () => {
|
|
84
|
+
for (const name of ACTIVITY_EVENTS) {
|
|
85
|
+
win.removeEventListener(name, this.markActivity, { capture: true })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private _detachActivityListeners() {
|
|
91
|
+
this._detachActivityListenersFn?.()
|
|
92
|
+
this._detachActivityListenersFn = null
|
|
20
93
|
}
|
|
21
94
|
|
|
22
95
|
@bind
|
|
@@ -462,6 +535,53 @@ export class InputsManager extends EditorManager {
|
|
|
462
535
|
return this.editor.getCollaborators().length > 0 // could we do this more efficiently?
|
|
463
536
|
}
|
|
464
537
|
|
|
538
|
+
private readonly _throttledActivityStamp = throttle(
|
|
539
|
+
() => {
|
|
540
|
+
const pointer = this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
|
|
541
|
+
if (!pointer) return
|
|
542
|
+
|
|
543
|
+
this.editor.run(
|
|
544
|
+
() => {
|
|
545
|
+
this.editor.store.put([{ ...pointer, lastActivityTimestamp: Date.now() }])
|
|
546
|
+
},
|
|
547
|
+
{ history: 'ignore' }
|
|
548
|
+
)
|
|
549
|
+
},
|
|
550
|
+
// Stay comfortably below the idle timeout so a continuously-active peer
|
|
551
|
+
// never flickers to idle between throttled stamps.
|
|
552
|
+
Math.max(
|
|
553
|
+
ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS,
|
|
554
|
+
Math.min(ACTIVITY_TIMESTAMP_THROTTLE_MS, this.editor.options.collaboratorIdleTimeoutMs / 3)
|
|
555
|
+
),
|
|
556
|
+
{ trailing: false }
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Mark the current user as active for collaborator presence. User input anywhere in the
|
|
561
|
+
* editor's tab counts as activity automatically; call this to make input that doesn't produce
|
|
562
|
+
* DOM events count too, so peers don't classify this user as idle or inactive while they're
|
|
563
|
+
* still interacting. Calls are throttled on the leading edge, so it's safe to call from
|
|
564
|
+
* high-frequency input events.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```ts
|
|
568
|
+
* // Gamepads are polled rather than event-driven, so stamp activity from the poll
|
|
569
|
+
* editor.timers.setInterval(() => {
|
|
570
|
+
* const isPressed = navigator
|
|
571
|
+
* .getGamepads()
|
|
572
|
+
* .some((pad) => pad?.buttons.some((button) => button.pressed))
|
|
573
|
+
* if (isPressed) editor.inputs.markActivity()
|
|
574
|
+
* }, 50)
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
@bind
|
|
578
|
+
markActivity() {
|
|
579
|
+
// Check for collaborators before consuming the throttle window, so input
|
|
580
|
+
// while alone doesn't delay the first stamp after a peer joins.
|
|
581
|
+
if (!this._getHasCollaborators()) return
|
|
582
|
+
this._throttledActivityStamp()
|
|
583
|
+
}
|
|
584
|
+
|
|
465
585
|
/**
|
|
466
586
|
* The previous point used for velocity calculation (updated each tick, not each pointer event).
|
|
467
587
|
* @internal
|
|
@@ -553,13 +673,12 @@ export class InputsManager extends EditorManager {
|
|
|
553
673
|
typeName: 'pointer',
|
|
554
674
|
x: pagePoint.x,
|
|
555
675
|
y: pagePoint.y,
|
|
676
|
+
// The activity timestamp is stamped by the container's input
|
|
677
|
+
// listeners (see `markActivity`), not by pointer position updates:
|
|
678
|
+
// synthetic moves, like the camera following another user, don't
|
|
679
|
+
// count as activity.
|
|
556
680
|
lastActivityTimestamp:
|
|
557
|
-
|
|
558
|
-
// update our last activity timestamp; otherwise, update it to the current timestamp.
|
|
559
|
-
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
|
|
560
|
-
? (this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
|
|
561
|
-
?.lastActivityTimestamp ?? Date.now())
|
|
562
|
-
: Date.now(),
|
|
681
|
+
this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ?? 0,
|
|
563
682
|
meta: {},
|
|
564
683
|
},
|
|
565
684
|
])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TLScribble, VecModel } from '@tldraw/tlschema'
|
|
1
|
+
import { TLPageId, TLScribble, VecModel } from '@tldraw/tlschema'
|
|
2
2
|
import { uniqueId } from '@tldraw/utils'
|
|
3
3
|
import { Vec } from '../../../primitives/Vec'
|
|
4
4
|
import type { Editor } from '../../Editor'
|
|
@@ -47,6 +47,7 @@ export interface ScribbleSessionOptions {
|
|
|
47
47
|
// Internal session state (not exported)
|
|
48
48
|
interface Session {
|
|
49
49
|
id: string
|
|
50
|
+
pageId: TLPageId
|
|
50
51
|
items: ScribbleItem[]
|
|
51
52
|
state: 'active' | 'stopping' | 'complete'
|
|
52
53
|
options: Required<Omit<ScribbleSessionOptions, 'id'>>
|
|
@@ -75,6 +76,7 @@ export class ScribbleManager {
|
|
|
75
76
|
const id = options.id ?? uniqueId()
|
|
76
77
|
const session: Session = {
|
|
77
78
|
id,
|
|
79
|
+
pageId: this.editor.getCurrentPageId(),
|
|
78
80
|
items: [],
|
|
79
81
|
state: 'active',
|
|
80
82
|
options: {
|
|
@@ -357,6 +359,15 @@ export class ScribbleManager {
|
|
|
357
359
|
if (this.sessions.size === 0 && currentScribbles.length === 0) return
|
|
358
360
|
|
|
359
361
|
this.editor.run(() => {
|
|
362
|
+
// Drop sessions from other pages, otherwise a still-fading laser trail
|
|
363
|
+
// keeps playing out over the page the user switched to (#10449).
|
|
364
|
+
const currentPageId = this.editor.getCurrentPageId()
|
|
365
|
+
for (const session of this.sessions.values()) {
|
|
366
|
+
if (session.pageId !== currentPageId) {
|
|
367
|
+
this.clearSession(session.id)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
360
371
|
// Tick all sessions
|
|
361
372
|
for (const session of this.sessions.values()) {
|
|
362
373
|
this.tickSession(session, elapsed)
|
|
@@ -206,12 +206,10 @@ export class BoundsSnaps {
|
|
|
206
206
|
const { editor } = this
|
|
207
207
|
return editor.store.createComputedCache<BoundsSnapPoint[], TLShape>('snapPoints', (shape) => {
|
|
208
208
|
const pageTransform = editor.getShapePageTransform(shape.id)
|
|
209
|
-
if (!pageTransform) return undefined
|
|
210
209
|
const boundsSnapGeometry = editor.getShapeUtil(shape).getBoundsSnapGeometry(shape)
|
|
211
210
|
const snapPoints =
|
|
212
211
|
boundsSnapGeometry.points ?? editor.getShapeGeometry(shape).bounds.cornersAndCenter
|
|
213
212
|
|
|
214
|
-
if (!pageTransform || !snapPoints) return undefined
|
|
215
213
|
return snapPoints.map((point, i) => {
|
|
216
214
|
const { x, y } = Mat.applyToPoint(pageTransform, point)
|
|
217
215
|
return { x, y, id: `${shape.id}:${i}` }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
2
|
-
import { objectMapKeys } from '@tldraw/utils'
|
|
2
|
+
import { iterateGraphemes, objectMapKeys } from '@tldraw/utils'
|
|
3
3
|
import type { Editor } from '../../Editor'
|
|
4
4
|
import { EditorManager } from '../EditorManager'
|
|
5
5
|
|
|
@@ -94,10 +94,6 @@ export interface TLMeasureTextSpanOpts {
|
|
|
94
94
|
|
|
95
95
|
const spaceCharacterRegex = /\s/
|
|
96
96
|
|
|
97
|
-
// Iterate by grapheme cluster (e.g. emoji sequences, flags, accented letters)
|
|
98
|
-
// rather than by code point, so a single glyph is measured as one unit.
|
|
99
|
-
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
100
|
-
|
|
101
97
|
const initialDefaultStyles = Object.freeze({
|
|
102
98
|
'overflow-wrap': 'break-word',
|
|
103
99
|
'word-break': 'auto',
|
|
@@ -309,7 +305,7 @@ export class TextManager extends EditorManager {
|
|
|
309
305
|
for (const childNode of element.childNodes) {
|
|
310
306
|
if (childNode.nodeType !== Node.TEXT_NODE) continue
|
|
311
307
|
|
|
312
|
-
for (const
|
|
308
|
+
for (const segment of iterateGraphemes(childNode.textContent ?? '')) {
|
|
313
309
|
// place the range around the grapheme we're interested in
|
|
314
310
|
range.setStart(textNode, idx)
|
|
315
311
|
range.setEnd(textNode, idx + segment.length)
|
|
@@ -142,6 +142,19 @@ describe('TickManager', () => {
|
|
|
142
142
|
expect(mockEmit).toHaveBeenCalledWith('frame', 0)
|
|
143
143
|
expect(mockEmit).toHaveBeenCalledWith('tick', 0)
|
|
144
144
|
})
|
|
145
|
+
|
|
146
|
+
it('should schedule the next frame even when a listener throws', () => {
|
|
147
|
+
const staleCancel = vi.fn()
|
|
148
|
+
tickManager.cancelRaf = staleCancel
|
|
149
|
+
mockEmit.mockImplementation(() => {
|
|
150
|
+
throw new Error('listener blew up')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// the error still reaches the caller, but the loop lives on
|
|
154
|
+
expect(() => tickManager.tick()).toThrow('listener blew up')
|
|
155
|
+
expect(mockRequestAnimationFrame).toHaveBeenCalled()
|
|
156
|
+
expect(tickManager.cancelRaf).not.toBe(staleCancel)
|
|
157
|
+
})
|
|
145
158
|
})
|
|
146
159
|
|
|
147
160
|
describe('dispose method', () => {
|
|
@@ -41,9 +41,15 @@ export class TickManager extends EditorManager {
|
|
|
41
41
|
const elapsed = now - this.now
|
|
42
42
|
this.now = now
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Schedule the next frame even if a listener throws. Otherwise a single throwing listener
|
|
45
|
+
// ends the loop for good, which silently stops every frame-driven behavior — camera
|
|
46
|
+
// animations, edge scrolling, pointer velocity, following — instead of failing once.
|
|
47
|
+
try {
|
|
48
|
+
this.editor.emit('frame', elapsed)
|
|
49
|
+
this.editor.emit('tick', elapsed)
|
|
50
|
+
} finally {
|
|
51
|
+
this.cancelRaf = throttleToNextFrame(this.tick)
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
dispose() {
|