@tldraw/editor 5.2.0-next.355d68cf24e8 → 5.2.0-next.43d958280e72
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 +44 -40
- 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/editor/Editor.js +91 -44
- package/dist-cjs/lib/editor/Editor.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/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/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/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 +44 -40
- 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/editor/Editor.mjs +91 -44
- package/dist-esm/lib/editor/Editor.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/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/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/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 +8 -8
- package/src/index.ts +2 -5
- package/src/lib/components/MenuClickCapture.tsx +8 -4
- package/src/lib/editor/Editor.ts +115 -53
- 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/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/hooks/useCanvasEvents.ts +19 -12
- package/src/lib/hooks/usePresence.ts +2 -2
- 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
package/editor.css
CHANGED
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
--tl-color-text-0: hsl(0, 0%, 11%);
|
|
183
183
|
--tl-color-text-1: hsl(0, 0%, 18%);
|
|
184
184
|
--tl-color-text-3: hsl(204, 4%, 45%);
|
|
185
|
+
--tl-color-text-disabled: hsl(204, 4%, 55%);
|
|
185
186
|
--tl-color-text-shadow: hsl(0, 0%, 100%);
|
|
186
187
|
--tl-color-text-highlight: hsl(52, 100%, 50%);
|
|
187
188
|
--tl-color-text-highlight-p3: color(display-p3 0.972 0.8205 0.05);
|
|
@@ -236,6 +237,7 @@
|
|
|
236
237
|
--tl-color-text-0: hsl(0, 9%, 94%);
|
|
237
238
|
--tl-color-text-1: hsl(0, 0%, 85%);
|
|
238
239
|
--tl-color-text-3: hsl(204, 4%, 75%);
|
|
240
|
+
--tl-color-text-disabled: hsl(204, 4%, 50%);
|
|
239
241
|
--tl-color-text-shadow: hsl(210, 13%, 18%);
|
|
240
242
|
--tl-color-text-highlight: hsl(52, 100%, 41%);
|
|
241
243
|
--tl-color-text-highlight-p3: color(display-p3 0.8078 0.6225 0.0312);
|
|
@@ -762,17 +764,17 @@ input,
|
|
|
762
764
|
.tl-rich-text ol {
|
|
763
765
|
text-align: left;
|
|
764
766
|
margin: 0;
|
|
765
|
-
padding-left:
|
|
767
|
+
padding-left: 1.625ch;
|
|
766
768
|
/* Some resets, like Tailwind, nix the list styling. */
|
|
767
769
|
list-style: revert;
|
|
768
770
|
}
|
|
769
771
|
|
|
770
772
|
.tl-rich-text ol:has(> li:nth-child(10)) {
|
|
771
|
-
padding-left:
|
|
773
|
+
padding-left: 2.625ch;
|
|
772
774
|
}
|
|
773
775
|
|
|
774
776
|
.tl-rich-text ol:has(> li:nth-child(100)) {
|
|
775
|
-
padding-left:
|
|
777
|
+
padding-left: 3.625ch;
|
|
776
778
|
}
|
|
777
779
|
|
|
778
780
|
.tl-rich-text h1,
|
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.2.0-next.
|
|
4
|
+
"version": "5.2.0-next.43d958280e72",
|
|
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.2.0-next.
|
|
53
|
-
"@tldraw/state-react": "5.2.0-next.
|
|
54
|
-
"@tldraw/store": "5.2.0-next.
|
|
55
|
-
"@tldraw/tlschema": "5.2.0-next.
|
|
56
|
-
"@tldraw/utils": "5.2.0-next.
|
|
57
|
-
"@tldraw/validate": "5.2.0-next.
|
|
52
|
+
"@tldraw/state": "5.2.0-next.43d958280e72",
|
|
53
|
+
"@tldraw/state-react": "5.2.0-next.43d958280e72",
|
|
54
|
+
"@tldraw/store": "5.2.0-next.43d958280e72",
|
|
55
|
+
"@tldraw/tlschema": "5.2.0-next.43d958280e72",
|
|
56
|
+
"@tldraw/utils": "5.2.0-next.43d958280e72",
|
|
57
|
+
"@tldraw/validate": "5.2.0-next.43d958280e72",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"react": "^19.2.1",
|
|
81
81
|
"react-dom": "^19.2.1",
|
|
82
82
|
"resize-observer-polyfill": "^1.5.1",
|
|
83
|
-
"vitest": "^
|
|
83
|
+
"vitest": "^4.1.7"
|
|
84
84
|
},
|
|
85
85
|
"module": "dist-esm/index.mjs",
|
|
86
86
|
"source": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ export {
|
|
|
128
128
|
export { SpatialIndexManager } from './lib/editor/managers/SpatialIndexManager/SpatialIndexManager'
|
|
129
129
|
export {
|
|
130
130
|
TextManager,
|
|
131
|
+
resolveLineHeightPx,
|
|
131
132
|
type BatchMeasurementRequest,
|
|
132
133
|
type TLMeasuredTextSize,
|
|
133
134
|
type TLMeasureTextOpts,
|
|
@@ -185,11 +186,7 @@ export {
|
|
|
185
186
|
type TLOverlay,
|
|
186
187
|
type TLOverlayUtilConstructor,
|
|
187
188
|
} from './lib/editor/overlays/OverlayUtil'
|
|
188
|
-
export {
|
|
189
|
-
ShapeIndicatorOverlayUtil,
|
|
190
|
-
strokeShapeIndicators,
|
|
191
|
-
type TLShapeIndicatorOverlay,
|
|
192
|
-
} from './lib/editor/overlays/ShapeIndicatorOverlayUtil'
|
|
189
|
+
export { strokeShapeIndicators } from './lib/editor/overlays/strokeShapeIndicators'
|
|
193
190
|
export {
|
|
194
191
|
getPerfectDashProps,
|
|
195
192
|
type PerfectDashTerminal,
|
|
@@ -6,6 +6,7 @@ import { useEditor } from '../hooks/useEditor'
|
|
|
6
6
|
import { Vec } from '../primitives/Vec'
|
|
7
7
|
import { releasePointerCapture, setPointerCapture } from '../utils/dom'
|
|
8
8
|
import { getPointerInfo } from '../utils/getPointerInfo'
|
|
9
|
+
import { getPointerEventButton } from '../utils/pointer'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* When a menu is open, this component prevents the user from interacting with the canvas.
|
|
@@ -68,18 +69,19 @@ export function MenuClickCapture() {
|
|
|
68
69
|
|
|
69
70
|
const handlePointerDown = useCallback(
|
|
70
71
|
(e: PointerEvent) => {
|
|
71
|
-
|
|
72
|
+
const button = getPointerEventButton(e)
|
|
73
|
+
if (button !== 0 && button !== 2) return
|
|
72
74
|
|
|
73
75
|
flushSync(() => setIsPointing(true))
|
|
74
76
|
setPointerCapture(e.currentTarget, e)
|
|
75
77
|
rPointerState.current = {
|
|
76
78
|
isDown: true,
|
|
77
79
|
isDragging: false,
|
|
78
|
-
button
|
|
80
|
+
button,
|
|
79
81
|
start: new Vec(e.clientX, e.clientY),
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
if (
|
|
84
|
+
if (button === 2) {
|
|
83
85
|
if (!editor.options.rightClickPanning) {
|
|
84
86
|
// Right-click panning off: close the open menu and swallow the native
|
|
85
87
|
// contextmenu that would otherwise briefly open a new one (causing a flash).
|
|
@@ -141,13 +143,15 @@ export function MenuClickCapture() {
|
|
|
141
143
|
|
|
142
144
|
const handlePointerUp = useCallback(
|
|
143
145
|
(e: PointerEvent) => {
|
|
144
|
-
const isStaticRightClick =
|
|
146
|
+
const isStaticRightClick =
|
|
147
|
+
rPointerState.current.button === 2 && !rPointerState.current.isDragging
|
|
145
148
|
|
|
146
149
|
editor.dispatch({
|
|
147
150
|
type: 'pointer',
|
|
148
151
|
target: 'canvas',
|
|
149
152
|
name: 'pointer_up',
|
|
150
153
|
...getPointerInfo(editor, e),
|
|
154
|
+
button: rPointerState.current.button === 2 ? 2 : getPointerEventButton(e),
|
|
151
155
|
})
|
|
152
156
|
|
|
153
157
|
if (isStaticRightClick && editor.options.rightClickPanning) {
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -409,9 +409,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
409
409
|
this._tickManager = new TickManager(this)
|
|
410
410
|
this.disposables.add(() => this._tickManager.dispose())
|
|
411
411
|
this.disposables.add(() => {
|
|
412
|
-
// Reset camera state to 'idle' so the store isn't left stuck at 'moving'
|
|
413
|
-
// when tick events stop (e.g. React strict mode disposes while camera is moving)
|
|
414
|
-
this.off('tick', this._decayCameraStateTimeout)
|
|
415
412
|
this._setCameraState('idle')
|
|
416
413
|
})
|
|
417
414
|
|
|
@@ -419,6 +416,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
419
416
|
this.disposables.add(() => this.fonts.dispose())
|
|
420
417
|
|
|
421
418
|
this.inputs = new InputsManager(this)
|
|
419
|
+
this.disposables.add(() => this.inputs.dispose())
|
|
422
420
|
this.performance = new PerformanceManager(this)
|
|
423
421
|
this.disposables.add(() => this.performance.dispose())
|
|
424
422
|
this.collaborators = new CollaboratorsManager(this)
|
|
@@ -1179,6 +1177,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1179
1177
|
this.store.dispose()
|
|
1180
1178
|
this.isDisposed = true
|
|
1181
1179
|
this.emit('dispose')
|
|
1180
|
+
this.removeAllListeners()
|
|
1182
1181
|
}
|
|
1183
1182
|
|
|
1184
1183
|
/* ------------------ Themes (shadowing the theme manager) ------------------ */
|
|
@@ -1551,6 +1550,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1551
1550
|
return this.history.getMarkIdMatching(idSubstring)
|
|
1552
1551
|
}
|
|
1553
1552
|
|
|
1553
|
+
/**
|
|
1554
|
+
* Whether the editor is currently replaying history (i.e. an undo or redo is being applied).
|
|
1555
|
+
*
|
|
1556
|
+
* @internal
|
|
1557
|
+
*/
|
|
1558
|
+
isReplayingHistory(): boolean {
|
|
1559
|
+
return this.history.isReplaying()
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1554
1562
|
/**
|
|
1555
1563
|
* Coalesces all changes since the given mark into a single change, removing any intermediate marks.
|
|
1556
1564
|
*
|
|
@@ -3068,8 +3076,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3068
3076
|
return baseCamera
|
|
3069
3077
|
}
|
|
3070
3078
|
|
|
3071
|
-
private _getFollowingPresence(targetUserId:
|
|
3072
|
-
const visited = [this.user.
|
|
3079
|
+
private _getFollowingPresence(targetUserId: TLUserId | null) {
|
|
3080
|
+
const visited = [this.user.getRecordId()]
|
|
3073
3081
|
const collaborators = this.getCollaborators()
|
|
3074
3082
|
let leaderPresence = null as null | TLInstancePresence
|
|
3075
3083
|
while (targetUserId && !visited.includes(targetUserId)) {
|
|
@@ -4029,16 +4037,34 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4029
4037
|
|
|
4030
4038
|
this.once('stop-camera-animation', cancel)
|
|
4031
4039
|
|
|
4040
|
+
const dirZ = direction.z ?? 0
|
|
4041
|
+
|
|
4032
4042
|
const moveCamera = (elapsed: number) => {
|
|
4033
4043
|
const { x: cx, y: cy, z: cz } = this.getCamera()
|
|
4034
|
-
|
|
4044
|
+
|
|
4045
|
+
// Pan movement from x/y direction
|
|
4046
|
+
const dx = (direction.x * (currentSpeed * elapsed)) / cz
|
|
4047
|
+
const dy = (direction.y * (currentSpeed * elapsed)) / cz
|
|
4048
|
+
|
|
4049
|
+
let newCx = cx + dx
|
|
4050
|
+
let newCy = cy + dy
|
|
4051
|
+
let newCz = cz
|
|
4052
|
+
|
|
4053
|
+
// animate zoom if z direction is passed in
|
|
4054
|
+
if (dirZ !== 0) {
|
|
4055
|
+
newCz = cz * (1 + dirZ * currentSpeed * elapsed)
|
|
4056
|
+
// Adjust x/y to keep the viewport center fixed while zooming
|
|
4057
|
+
const center = this.getViewportScreenCenter()
|
|
4058
|
+
newCx += center.x / newCz - center.x / cz
|
|
4059
|
+
newCy += center.y / newCz - center.y / cz
|
|
4060
|
+
}
|
|
4035
4061
|
|
|
4036
4062
|
// Apply friction
|
|
4037
4063
|
currentSpeed *= 1 - friction
|
|
4038
4064
|
if (currentSpeed < speedThreshold) {
|
|
4039
4065
|
cancel()
|
|
4040
4066
|
} else {
|
|
4041
|
-
this._setCamera(new Vec(
|
|
4067
|
+
this._setCamera(new Vec(newCx, newCy, newCz))
|
|
4042
4068
|
}
|
|
4043
4069
|
}
|
|
4044
4070
|
|
|
@@ -4060,7 +4086,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4060
4086
|
* @param opts - The camera move options.
|
|
4061
4087
|
* @public
|
|
4062
4088
|
*/
|
|
4063
|
-
zoomToUser(userId:
|
|
4089
|
+
zoomToUser(userId: TLUserId, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this {
|
|
4064
4090
|
const presence = this.getCollaborators().find((c) => c.userId === userId)
|
|
4065
4091
|
|
|
4066
4092
|
if (!presence) return this
|
|
@@ -4432,11 +4458,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4432
4458
|
*
|
|
4433
4459
|
* @public
|
|
4434
4460
|
*/
|
|
4435
|
-
startFollowingUser(userId:
|
|
4461
|
+
startFollowingUser(userId: TLUserId): this {
|
|
4436
4462
|
// if we were already following someone, stop following them
|
|
4437
4463
|
this.stopFollowingUser()
|
|
4438
4464
|
|
|
4439
|
-
const thisUserId = this.user.
|
|
4465
|
+
const thisUserId = this.user.getExternalId()
|
|
4440
4466
|
|
|
4441
4467
|
if (!thisUserId) {
|
|
4442
4468
|
console.warn('You should set the userId for the current instance before following a user')
|
|
@@ -8084,32 +8110,53 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8084
8110
|
* @public
|
|
8085
8111
|
*/
|
|
8086
8112
|
resizeShape(shape: TLShapeId | TLShape, scale: VecLike, opts: TLResizeShapeOptions = {}): this {
|
|
8113
|
+
const partial = this.getResizeShapePartial(shape, scale, opts)
|
|
8114
|
+
if (partial) this.updateShapes([partial])
|
|
8115
|
+
return this
|
|
8116
|
+
}
|
|
8117
|
+
|
|
8118
|
+
/**
|
|
8119
|
+
* Get the update for a resized shape without committing it to the store. Interactions that
|
|
8120
|
+
* resize many shapes at once use this to collect all of the updates and commit them in a
|
|
8121
|
+
* single batch. Returns null when there is nothing to update.
|
|
8122
|
+
*
|
|
8123
|
+
* Shapes that are rotated out of alignment with the scale axis cannot be resized with a
|
|
8124
|
+
* single update; those shapes are resized immediately (as `resizeShape` would do) and null
|
|
8125
|
+
* is returned.
|
|
8126
|
+
*
|
|
8127
|
+
* @internal
|
|
8128
|
+
*/
|
|
8129
|
+
getResizeShapePartial(
|
|
8130
|
+
shape: TLShapeId | TLShape,
|
|
8131
|
+
scale: VecLike,
|
|
8132
|
+
opts: TLResizeShapeOptions = {}
|
|
8133
|
+
): TLShapePartial | null {
|
|
8087
8134
|
const id = typeof shape === 'string' ? shape : shape.id
|
|
8088
|
-
if (this.getIsReadonly()) return
|
|
8135
|
+
if (this.getIsReadonly()) return null
|
|
8089
8136
|
|
|
8090
8137
|
if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y)
|
|
8091
8138
|
if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1)
|
|
8092
8139
|
|
|
8093
8140
|
const initialShape = opts.initialShape ?? this.getShape(id)
|
|
8094
|
-
if (!initialShape) return
|
|
8141
|
+
if (!initialShape) return null
|
|
8095
8142
|
|
|
8096
8143
|
const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center
|
|
8097
|
-
if (!scaleOrigin) return
|
|
8144
|
+
if (!scaleOrigin) return null
|
|
8098
8145
|
|
|
8099
8146
|
const pageTransform = opts.initialPageTransform
|
|
8100
8147
|
? Mat.Cast(opts.initialPageTransform)
|
|
8101
8148
|
: this.getShapePageTransform(id)
|
|
8102
|
-
if (!pageTransform) return
|
|
8149
|
+
if (!pageTransform) return null
|
|
8103
8150
|
|
|
8104
8151
|
const pageRotation = pageTransform.rotation()
|
|
8105
8152
|
|
|
8106
|
-
if (pageRotation == null) return
|
|
8153
|
+
if (pageRotation == null) return null
|
|
8107
8154
|
|
|
8108
8155
|
const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
|
|
8109
8156
|
|
|
8110
8157
|
const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
|
|
8111
8158
|
|
|
8112
|
-
if (!initialBounds) return
|
|
8159
|
+
if (!initialBounds) return null
|
|
8113
8160
|
|
|
8114
8161
|
const isAspectRatioLocked =
|
|
8115
8162
|
opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape)
|
|
@@ -8119,7 +8166,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8119
8166
|
// from whichever axis is being scaled the least, to avoid the shape getting bigger
|
|
8120
8167
|
// than the bounds of the selection
|
|
8121
8168
|
// const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y))
|
|
8122
|
-
|
|
8169
|
+
this._resizeUnalignedShape(id, scale, {
|
|
8123
8170
|
...opts,
|
|
8124
8171
|
initialBounds,
|
|
8125
8172
|
scaleOrigin,
|
|
@@ -8128,6 +8175,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8128
8175
|
isAspectRatioLocked,
|
|
8129
8176
|
initialShape,
|
|
8130
8177
|
})
|
|
8178
|
+
return null
|
|
8131
8179
|
}
|
|
8132
8180
|
|
|
8133
8181
|
const util = this.getShapeUtil(initialShape)
|
|
@@ -8140,7 +8188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8140
8188
|
}
|
|
8141
8189
|
}
|
|
8142
8190
|
|
|
8143
|
-
let
|
|
8191
|
+
let workingShape: TLShape | null = null
|
|
8144
8192
|
|
|
8145
8193
|
if (util.onResize && util.canResize(initialShape)) {
|
|
8146
8194
|
// get the model changes from the shape util
|
|
@@ -8172,7 +8220,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8172
8220
|
// need to adjust the shape's x and y points in case the parent has moved since start of resizing
|
|
8173
8221
|
const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint)
|
|
8174
8222
|
|
|
8175
|
-
|
|
8223
|
+
workingShape = initialShape
|
|
8176
8224
|
if (!opts.skipStartAndEndCallbacks) {
|
|
8177
8225
|
workingShape = applyPartialToRecordWithProps(
|
|
8178
8226
|
initialShape,
|
|
@@ -8194,10 +8242,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8194
8242
|
}
|
|
8195
8243
|
)
|
|
8196
8244
|
|
|
8197
|
-
if (resizedShape) {
|
|
8198
|
-
didResize = true
|
|
8199
|
-
}
|
|
8200
|
-
|
|
8201
8245
|
workingShape = applyPartialToRecordWithProps(workingShape, {
|
|
8202
8246
|
id,
|
|
8203
8247
|
type: initialShape.type as any,
|
|
@@ -8213,40 +8257,47 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8213
8257
|
)
|
|
8214
8258
|
}
|
|
8215
8259
|
|
|
8216
|
-
|
|
8260
|
+
if (resizedShape) {
|
|
8261
|
+
return workingShape
|
|
8262
|
+
}
|
|
8217
8263
|
}
|
|
8218
8264
|
|
|
8219
|
-
|
|
8220
|
-
|
|
8265
|
+
// the shape was not resized by its util, so reposition it (rather than resizing it)
|
|
8266
|
+
// based on where its resized center would be
|
|
8221
8267
|
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8268
|
+
const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
|
|
8269
|
+
// get the model changes from the shape util
|
|
8270
|
+
const newPageCenter = this._scalePagePoint(
|
|
8271
|
+
initialPageCenter,
|
|
8272
|
+
scaleOrigin,
|
|
8273
|
+
scale,
|
|
8274
|
+
scaleAxisRotation
|
|
8275
|
+
)
|
|
8230
8276
|
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8277
|
+
const initialPageCenterInParentSpace = this.getPointInParentSpace(
|
|
8278
|
+
initialShape.id,
|
|
8279
|
+
initialPageCenter
|
|
8280
|
+
)
|
|
8281
|
+
const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
|
|
8236
8282
|
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8283
|
+
const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
|
|
8284
|
+
|
|
8285
|
+
if (workingShape) {
|
|
8286
|
+
// the util's onResize ran but returned no change; keep the working update (which may
|
|
8287
|
+
// include changes from onResizeStart / onResizeEnd) and reposition the shape
|
|
8288
|
+
return {
|
|
8289
|
+
...workingShape,
|
|
8290
|
+
x: initialShape.x + delta.x,
|
|
8291
|
+
y: initialShape.y + delta.y,
|
|
8292
|
+
}
|
|
8247
8293
|
}
|
|
8248
8294
|
|
|
8249
|
-
return
|
|
8295
|
+
return {
|
|
8296
|
+
id,
|
|
8297
|
+
type: initialShape.type as any,
|
|
8298
|
+
x: initialShape.x + delta.x,
|
|
8299
|
+
y: initialShape.y + delta.y,
|
|
8300
|
+
}
|
|
8250
8301
|
}
|
|
8251
8302
|
|
|
8252
8303
|
/** @internal */
|
|
@@ -11173,7 +11224,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11173
11224
|
inputs.setIsDragging(false)
|
|
11174
11225
|
|
|
11175
11226
|
// If pen mode is off but we're not already in pen mode, turn that on
|
|
11176
|
-
if (!isPenMode && isPen)
|
|
11227
|
+
if (!isPenMode && isPen) {
|
|
11228
|
+
this.updateInstanceState({ isPenMode: true })
|
|
11229
|
+
// Once pen mode is on, touch input is ignored, so we discard the
|
|
11230
|
+
// in-progress touch interaction .
|
|
11231
|
+
this.interrupt()
|
|
11232
|
+
}
|
|
11177
11233
|
|
|
11178
11234
|
// On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser
|
|
11179
11235
|
if (info.button === STYLUS_ERASER_BUTTON) {
|
|
@@ -11323,7 +11379,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11323
11379
|
// Don't pass right-click panning events to the state chart
|
|
11324
11380
|
// as it causes unintended shape selection on release
|
|
11325
11381
|
if (slideSpeed > 0) {
|
|
11326
|
-
this.slideCamera({
|
|
11382
|
+
this.slideCamera({
|
|
11383
|
+
speed: slideSpeed,
|
|
11384
|
+
direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
|
|
11385
|
+
})
|
|
11327
11386
|
}
|
|
11328
11387
|
this._selectedShapeIdsAtPointerDown = []
|
|
11329
11388
|
this._didCaptureSelectionAtPointerDown = false
|
|
@@ -11332,7 +11391,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11332
11391
|
}
|
|
11333
11392
|
|
|
11334
11393
|
if (slideSpeed > 0) {
|
|
11335
|
-
this.slideCamera({
|
|
11394
|
+
this.slideCamera({
|
|
11395
|
+
speed: slideSpeed,
|
|
11396
|
+
direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
|
|
11397
|
+
})
|
|
11336
11398
|
}
|
|
11337
11399
|
} else {
|
|
11338
11400
|
if (info.button === STYLUS_ERASER_BUTTON) {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PageRecordType,
|
|
3
|
+
createUserId,
|
|
4
|
+
type TLInstancePresence,
|
|
5
|
+
type TLUserId,
|
|
6
|
+
} from '@tldraw/tlschema'
|
|
2
7
|
import { vi } from 'vitest'
|
|
8
|
+
import { createTLStore } from '../../../config/createTLStore'
|
|
3
9
|
import type { Editor } from '../../Editor'
|
|
4
10
|
import { CollaboratorsManager } from './CollaboratorsManager'
|
|
5
11
|
|
|
6
12
|
const currentPageId = PageRecordType.createId('page')
|
|
7
13
|
|
|
8
|
-
function createPresence(userId:
|
|
14
|
+
function createPresence(userId: TLUserId): TLInstancePresence {
|
|
9
15
|
return {
|
|
10
16
|
typeName: 'instance_presence',
|
|
11
17
|
id: `instance_presence:${userId}` as TLInstancePresence['id'],
|
|
@@ -34,6 +40,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
|
|
|
34
40
|
}))
|
|
35
41
|
const userGetId = vi.fn(() => 'current-user')
|
|
36
42
|
|
|
43
|
+
const store = createTLStore()
|
|
44
|
+
store.put(presences)
|
|
45
|
+
|
|
37
46
|
const editor = {
|
|
38
47
|
options: {
|
|
39
48
|
collaboratorCheckIntervalMs: 1000,
|
|
@@ -45,14 +54,9 @@ function createEditor(presences: TLInstancePresence[] = []) {
|
|
|
45
54
|
},
|
|
46
55
|
user: {
|
|
47
56
|
getId: userGetId,
|
|
57
|
+
getRecordId: () => createUserId(userGetId()),
|
|
48
58
|
},
|
|
49
|
-
store
|
|
50
|
-
query: {
|
|
51
|
-
records: vi.fn(() => ({
|
|
52
|
-
get: () => presences,
|
|
53
|
-
})),
|
|
54
|
-
},
|
|
55
|
-
},
|
|
59
|
+
store,
|
|
56
60
|
getInstanceState,
|
|
57
61
|
getCurrentPageId: vi.fn(() => currentPageId),
|
|
58
62
|
} as unknown as Editor
|
|
@@ -88,10 +92,19 @@ describe(CollaboratorsManager, () => {
|
|
|
88
92
|
expect(setInterval).toHaveBeenCalledTimes(1)
|
|
89
93
|
})
|
|
90
94
|
|
|
95
|
+
it("excludes the local user's own other sessions", () => {
|
|
96
|
+
const ownSession = createPresence(createUserId('current-user'))
|
|
97
|
+
const peer = createPresence(createUserId('peer'))
|
|
98
|
+
const { editor } = createEditor([ownSession, peer])
|
|
99
|
+
const manager = new CollaboratorsManager(editor)
|
|
100
|
+
|
|
101
|
+
expect(manager.getCollaborators()).toEqual([peer])
|
|
102
|
+
})
|
|
103
|
+
|
|
91
104
|
it('reads instance state once when filtering visible collaborators', () => {
|
|
92
105
|
const { editor, getInstanceState } = createEditor([
|
|
93
|
-
createPresence('user-1'),
|
|
94
|
-
createPresence('user-2'),
|
|
106
|
+
createPresence(createUserId('user-1')),
|
|
107
|
+
createPresence(createUserId('user-2')),
|
|
95
108
|
])
|
|
96
109
|
const manager = new CollaboratorsManager(editor)
|
|
97
110
|
|
|
@@ -101,9 +114,9 @@ describe(CollaboratorsManager, () => {
|
|
|
101
114
|
})
|
|
102
115
|
|
|
103
116
|
it('hides idle collaborators that are following us', () => {
|
|
104
|
-
const presence = createPresence('peer')
|
|
117
|
+
const presence = createPresence(createUserId('peer'))
|
|
105
118
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
106
|
-
presence.followingUserId = 'current-user'
|
|
119
|
+
presence.followingUserId = createUserId('current-user')
|
|
107
120
|
const { editor } = createEditor([presence])
|
|
108
121
|
const manager = new CollaboratorsManager(editor)
|
|
109
122
|
|
|
@@ -111,9 +124,9 @@ describe(CollaboratorsManager, () => {
|
|
|
111
124
|
})
|
|
112
125
|
|
|
113
126
|
it('shows idle collaborators that are following us when they have a chat message', () => {
|
|
114
|
-
const presence = createPresence('peer')
|
|
127
|
+
const presence = createPresence(createUserId('peer'))
|
|
115
128
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
116
|
-
presence.followingUserId = 'current-user'
|
|
129
|
+
presence.followingUserId = createUserId('current-user')
|
|
117
130
|
presence.chatMessage = 'hi'
|
|
118
131
|
const { editor } = createEditor([presence])
|
|
119
132
|
const manager = new CollaboratorsManager(editor)
|
|
@@ -122,11 +135,25 @@ describe(CollaboratorsManager, () => {
|
|
|
122
135
|
})
|
|
123
136
|
|
|
124
137
|
it('shows idle collaborators that are not following us', () => {
|
|
125
|
-
const presence = createPresence('peer')
|
|
138
|
+
const presence = createPresence(createUserId('peer'))
|
|
126
139
|
presence.lastActivityTimestamp = Date.now() - 4000
|
|
127
140
|
const { editor } = createEditor([presence])
|
|
128
141
|
const manager = new CollaboratorsManager(editor)
|
|
129
142
|
|
|
130
143
|
expect(manager.getVisibleCollaborators()).toHaveLength(1)
|
|
131
144
|
})
|
|
145
|
+
|
|
146
|
+
it('shows newly-joined collaborators that have not recorded any activity yet', () => {
|
|
147
|
+
// A peer who has joined but not moved their pointer broadcasts the default
|
|
148
|
+
// `lastActivityTimestamp` of 0. They should still be treated as active so
|
|
149
|
+
// they appear in the people menu / face pile. See issue #9017.
|
|
150
|
+
const zero = createPresence(createUserId('zero'))
|
|
151
|
+
zero.lastActivityTimestamp = 0
|
|
152
|
+
const nullish = createPresence(createUserId('nullish'))
|
|
153
|
+
nullish.lastActivityTimestamp = null
|
|
154
|
+
const { editor } = createEditor([zero, nullish])
|
|
155
|
+
const manager = new CollaboratorsManager(editor)
|
|
156
|
+
|
|
157
|
+
expect(manager.getVisibleCollaborators()).toHaveLength(2)
|
|
158
|
+
})
|
|
132
159
|
})
|
|
@@ -37,7 +37,7 @@ export class CollaboratorsManager {
|
|
|
37
37
|
@computed
|
|
38
38
|
private _getCollaboratorsQuery() {
|
|
39
39
|
return this.editor.store.query.records('instance_presence', () => ({
|
|
40
|
-
userId: { neq: this.editor.user.
|
|
40
|
+
userId: { neq: this.editor.user.getRecordId() },
|
|
41
41
|
}))
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -88,14 +88,17 @@ export class CollaboratorsManager {
|
|
|
88
88
|
if (!collaborators.length) return EMPTY_ARRAY
|
|
89
89
|
|
|
90
90
|
const { followingUserId, highlightedUserIds } = this.editor.getInstanceState()
|
|
91
|
-
const currentUserId = this.editor.user.
|
|
91
|
+
const currentUserId = this.editor.user.getRecordId()
|
|
92
92
|
|
|
93
93
|
return collaborators.filter((presence) => {
|
|
94
94
|
const { lastActivityTimestamp, userId, chatMessage } = presence
|
|
95
95
|
|
|
96
|
-
// Treat a missing `lastActivityTimestamp` as "active right now"
|
|
97
|
-
// so newly-joined peers aren't immediately classified as
|
|
98
|
-
|
|
96
|
+
// Treat a missing or zero `lastActivityTimestamp` as "active right now"
|
|
97
|
+
// (elapsed = 0) so newly-joined peers aren't immediately classified as
|
|
98
|
+
// idle/inactive. The broadcast default for peers who haven't moved their
|
|
99
|
+
// pointer yet is `0` (e.g. someone on a touch device who joins and just
|
|
100
|
+
// watches), so a plain `?? now` would leave them hidden. See issue #9017.
|
|
101
|
+
const elapsed = lastActivityTimestamp ? Math.max(0, now - lastActivityTimestamp) : 0
|
|
99
102
|
|
|
100
103
|
if (elapsed > collaboratorInactiveTimeoutMs) {
|
|
101
104
|
// Inactive: If they're inactive, only show if we're following them or they're highlighted
|
|
@@ -15,12 +15,14 @@ import { FontManager } from './FontManager'
|
|
|
15
15
|
vi.mock('../../Editor')
|
|
16
16
|
|
|
17
17
|
// Mock globals
|
|
18
|
-
global.FontFace = vi.fn().mockImplementation((family, src, descriptors)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
global.FontFace = vi.fn().mockImplementation(function (family: any, src: any, descriptors: any) {
|
|
19
|
+
return {
|
|
20
|
+
family,
|
|
21
|
+
src,
|
|
22
|
+
...descriptors,
|
|
23
|
+
load: vi.fn(() => Promise.resolve()),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
24
26
|
|
|
25
27
|
Object.defineProperty(global.document, 'fonts', {
|
|
26
28
|
value: {
|
|
@@ -200,9 +202,11 @@ describe('FontManager', () => {
|
|
|
200
202
|
const font = createMockFont()
|
|
201
203
|
const error = new Error('Font load failed')
|
|
202
204
|
|
|
203
|
-
;(global.FontFace as Mock).
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
;(global.FontFace as Mock).mockImplementationOnce(function () {
|
|
206
|
+
return {
|
|
207
|
+
family: font.family,
|
|
208
|
+
load: vi.fn(() => Promise.reject(error)),
|
|
209
|
+
}
|
|
206
210
|
})
|
|
207
211
|
|
|
208
212
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|