@tldraw/editor 4.4.0-next.55b4db2171bd → 4.4.0-next.f181afb0ab39
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 +175 -11
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
- package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +201 -0
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +29 -14
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +4 -1
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +2 -3
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +29 -0
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/options.js +1 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/utils/collaboratorState.js +42 -0
- package/dist-cjs/lib/utils/collaboratorState.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 +175 -11
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +181 -0
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +29 -14
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +4 -1
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +2 -3
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +33 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +1 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
- package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +6 -0
- package/package.json +7 -7
- package/src/index.ts +2 -0
- package/src/lib/components/LiveCollaborators.tsx +26 -37
- package/src/lib/components/default-components/CanvasShapeIndicators.tsx +244 -0
- package/src/lib/components/default-components/DefaultCanvas.tsx +15 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
- package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
- package/src/lib/config/TLUserPreferences.test.ts +1 -0
- package/src/lib/config/TLUserPreferences.ts +8 -0
- package/src/lib/editor/Editor.ts +10 -1
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +2 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +44 -0
- package/src/lib/hooks/usePeerIds.ts +46 -1
- package/src/lib/options.ts +7 -0
- package/src/lib/utils/collaboratorState.ts +54 -0
- package/src/version.ts +3 -3
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -621
|
@@ -6,6 +6,11 @@ import { useEditor } from '../hooks/useEditor'
|
|
|
6
6
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
|
7
7
|
import { usePeerIds } from '../hooks/usePeerIds'
|
|
8
8
|
import { usePresence } from '../hooks/usePresence'
|
|
9
|
+
import {
|
|
10
|
+
CollaboratorState,
|
|
11
|
+
getCollaboratorStateFromElapsedTime,
|
|
12
|
+
shouldShowCollaborator,
|
|
13
|
+
} from '../utils/collaboratorState'
|
|
9
14
|
|
|
10
15
|
export const LiveCollaborators = track(function Collaborators() {
|
|
11
16
|
const peerIds = usePeerIds()
|
|
@@ -26,30 +31,8 @@ const CollaboratorGuard = track(function CollaboratorGuard({
|
|
|
26
31
|
return null
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const { followingUserId, highlightedUserIds } = editor.getInstanceState()
|
|
32
|
-
// If they're inactive and unless we're following them or they're highlighted, hide them
|
|
33
|
-
if (!(followingUserId === presence.userId || highlightedUserIds.includes(presence.userId))) {
|
|
34
|
-
return null
|
|
35
|
-
}
|
|
36
|
-
break
|
|
37
|
-
}
|
|
38
|
-
case 'idle': {
|
|
39
|
-
const { highlightedUserIds } = editor.getInstanceState()
|
|
40
|
-
// If they're idle and following us and unless they have a chat message or are highlighted, hide them
|
|
41
|
-
if (
|
|
42
|
-
presence.followingUserId === editor.user.getId() &&
|
|
43
|
-
!(presence.chatMessage || highlightedUserIds.includes(presence.userId))
|
|
44
|
-
) {
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
case 'active': {
|
|
50
|
-
// If they're active, show them
|
|
51
|
-
break
|
|
52
|
-
}
|
|
34
|
+
if (!shouldShowCollaborator(editor, presence, collaboratorState)) {
|
|
35
|
+
return null
|
|
53
36
|
}
|
|
54
37
|
|
|
55
38
|
return <Collaborator latestPresence={presence} />
|
|
@@ -137,7 +120,16 @@ const Collaborator = track(function Collaborator({
|
|
|
137
120
|
) : null}
|
|
138
121
|
{CollaboratorShapeIndicator &&
|
|
139
122
|
selectedShapeIds
|
|
140
|
-
.filter((id) =>
|
|
123
|
+
.filter((id) => {
|
|
124
|
+
// Skip hidden shapes
|
|
125
|
+
if (editor.isShapeHidden(id)) return false
|
|
126
|
+
// Only render SVG indicators for shapes that use legacy indicators
|
|
127
|
+
// Canvas-based indicators are handled by CanvasShapeIndicators
|
|
128
|
+
const shape = editor.getShape(id)
|
|
129
|
+
if (!shape) return false
|
|
130
|
+
const util = editor.getShapeUtil(shape)
|
|
131
|
+
return util.useLegacyIndicator()
|
|
132
|
+
})
|
|
141
133
|
.map((shapeId) => (
|
|
142
134
|
<CollaboratorShapeIndicator
|
|
143
135
|
className="tl-collaborator__shape-indicator"
|
|
@@ -152,24 +144,21 @@ const Collaborator = track(function Collaborator({
|
|
|
152
144
|
)
|
|
153
145
|
})
|
|
154
146
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
? 'idle'
|
|
160
|
-
: 'active'
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function useCollaboratorState(editor: Editor, latestPresence: TLInstancePresence | null) {
|
|
147
|
+
function useCollaboratorState(
|
|
148
|
+
editor: Editor,
|
|
149
|
+
latestPresence: TLInstancePresence | null
|
|
150
|
+
): CollaboratorState {
|
|
164
151
|
const rLastActivityTimestamp = useRef(latestPresence?.lastActivityTimestamp ?? -1)
|
|
165
152
|
|
|
166
|
-
const [state, setState] = useState<
|
|
167
|
-
|
|
153
|
+
const [state, setState] = useState<CollaboratorState>(() =>
|
|
154
|
+
getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
|
|
168
155
|
)
|
|
169
156
|
|
|
170
157
|
useEffect(() => {
|
|
171
158
|
const interval = editor.timers.setInterval(() => {
|
|
172
|
-
setState(
|
|
159
|
+
setState(
|
|
160
|
+
getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
|
|
161
|
+
)
|
|
173
162
|
}, editor.options.collaboratorCheckIntervalMs)
|
|
174
163
|
|
|
175
164
|
return () => clearInterval(interval)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useComputed, useQuickReactor } from '@tldraw/state-react'
|
|
2
|
+
import { createComputedCache } from '@tldraw/store'
|
|
3
|
+
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
4
|
+
import { dedupe, isEqual } from '@tldraw/utils'
|
|
5
|
+
import { memo, useEffect, useRef } from 'react'
|
|
6
|
+
import { Editor } from '../../editor/Editor'
|
|
7
|
+
import { TLIndicatorPath } from '../../editor/shapes/ShapeUtil'
|
|
8
|
+
import { useEditor } from '../../hooks/useEditor'
|
|
9
|
+
import { useIsDarkMode } from '../../hooks/useIsDarkMode'
|
|
10
|
+
import { useActivePeerIds$ } from '../../hooks/usePeerIds'
|
|
11
|
+
|
|
12
|
+
interface CollaboratorIndicatorData {
|
|
13
|
+
color: string
|
|
14
|
+
shapeIds: TLShapeId[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const indicatorPathCache = createComputedCache(
|
|
18
|
+
'indicatorPath',
|
|
19
|
+
(editor: Editor, shape: TLShape) => {
|
|
20
|
+
const util = editor.getShapeUtil(shape)
|
|
21
|
+
return util.getIndicatorPath(shape)
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const getIndicatorPath = (editor: Editor, shape: TLShape) => {
|
|
26
|
+
return indicatorPathCache.get(editor, shape.id)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderShapeIndicator(
|
|
30
|
+
ctx: CanvasRenderingContext2D,
|
|
31
|
+
editor: Editor,
|
|
32
|
+
shapeId: TLShapeId,
|
|
33
|
+
renderingShapeIds: Set<TLShapeId>
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!renderingShapeIds.has(shapeId)) return false
|
|
36
|
+
|
|
37
|
+
const shape = editor.getShape(shapeId)
|
|
38
|
+
if (!shape || shape.isLocked) return false
|
|
39
|
+
|
|
40
|
+
const pageTransform = editor.getShapePageTransform(shape)
|
|
41
|
+
if (!pageTransform) return false
|
|
42
|
+
|
|
43
|
+
const indicatorPath = getIndicatorPath(editor, shape)
|
|
44
|
+
if (!indicatorPath) return false
|
|
45
|
+
|
|
46
|
+
ctx.save()
|
|
47
|
+
ctx.transform(
|
|
48
|
+
pageTransform.a,
|
|
49
|
+
pageTransform.b,
|
|
50
|
+
pageTransform.c,
|
|
51
|
+
pageTransform.d,
|
|
52
|
+
pageTransform.e,
|
|
53
|
+
pageTransform.f
|
|
54
|
+
)
|
|
55
|
+
renderIndicatorPath(ctx, indicatorPath)
|
|
56
|
+
ctx.restore()
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderIndicatorPath(ctx: CanvasRenderingContext2D, indicatorPath: TLIndicatorPath) {
|
|
62
|
+
if (indicatorPath instanceof Path2D) {
|
|
63
|
+
ctx.stroke(indicatorPath)
|
|
64
|
+
} else {
|
|
65
|
+
const { path, clipPath, additionalPaths } = indicatorPath
|
|
66
|
+
|
|
67
|
+
if (clipPath) {
|
|
68
|
+
ctx.save()
|
|
69
|
+
ctx.clip(clipPath, 'evenodd')
|
|
70
|
+
ctx.stroke(path)
|
|
71
|
+
ctx.restore()
|
|
72
|
+
} else {
|
|
73
|
+
ctx.stroke(path)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (additionalPaths) {
|
|
77
|
+
for (const additionalPath of additionalPaths) {
|
|
78
|
+
ctx.stroke(additionalPath)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @internal @react */
|
|
85
|
+
export const CanvasShapeIndicators = memo(function CanvasShapeIndicators() {
|
|
86
|
+
const editor = useEditor()
|
|
87
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
88
|
+
|
|
89
|
+
// Cache the selected color to avoid getComputedStyle on every render
|
|
90
|
+
const rSelectedColor = useRef<string | null>(null)
|
|
91
|
+
const isDarkMode = useIsDarkMode()
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const timer = editor.timers.setTimeout(() => {
|
|
95
|
+
rSelectedColor.current = null
|
|
96
|
+
}, 0)
|
|
97
|
+
return () => clearTimeout(timer)
|
|
98
|
+
}, [isDarkMode, editor])
|
|
99
|
+
|
|
100
|
+
// Get active peer IDs (already handles time-based state transitions)
|
|
101
|
+
const activePeerIds$ = useActivePeerIds$()
|
|
102
|
+
|
|
103
|
+
const $renderData = useComputed(
|
|
104
|
+
'indicator render data',
|
|
105
|
+
() => {
|
|
106
|
+
const renderingShapeIds = new Set(editor.getRenderingShapes().map((s) => s.id))
|
|
107
|
+
|
|
108
|
+
// Compute ids to display for selected/hovered shapes
|
|
109
|
+
const idsToDisplay = new Set<TLShapeId>()
|
|
110
|
+
const instanceState = editor.getInstanceState()
|
|
111
|
+
const isChangingStyle = instanceState.isChangingStyle
|
|
112
|
+
const isIdleOrEditing = editor.isInAny('select.idle', 'select.editing_shape')
|
|
113
|
+
const isInSelectState = editor.isInAny(
|
|
114
|
+
'select.brushing',
|
|
115
|
+
'select.scribble_brushing',
|
|
116
|
+
'select.pointing_shape',
|
|
117
|
+
'select.pointing_selection',
|
|
118
|
+
'select.pointing_handle'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (!isChangingStyle && (isIdleOrEditing || isInSelectState)) {
|
|
122
|
+
for (const id of editor.getSelectedShapeIds()) {
|
|
123
|
+
idsToDisplay.add(id)
|
|
124
|
+
}
|
|
125
|
+
if (isIdleOrEditing && instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
|
|
126
|
+
const hovered = editor.getHoveredShapeId()
|
|
127
|
+
if (hovered) idsToDisplay.add(hovered)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compute hinting shape ids
|
|
132
|
+
const hintingShapeIds = dedupe(editor.getHintingShapeIds())
|
|
133
|
+
|
|
134
|
+
// Compute collaborator indicators
|
|
135
|
+
const collaboratorIndicators: CollaboratorIndicatorData[] = []
|
|
136
|
+
const currentPageId = editor.getCurrentPageId()
|
|
137
|
+
const activePeerIds = activePeerIds$.get()
|
|
138
|
+
|
|
139
|
+
const collaborators = editor.getCollaborators()
|
|
140
|
+
for (const peerId of activePeerIds.values()) {
|
|
141
|
+
// Skip collaborators on different pages
|
|
142
|
+
const presence = collaborators.find((c) => c.userId === peerId)
|
|
143
|
+
if (!presence || presence.currentPageId !== currentPageId) continue
|
|
144
|
+
|
|
145
|
+
// Filter to shapes that are visible and on the current rendering set
|
|
146
|
+
const visibleShapeIds = presence.selectedShapeIds.filter(
|
|
147
|
+
(id) => renderingShapeIds.has(id) && !editor.isShapeHidden(id)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (visibleShapeIds.length > 0) {
|
|
151
|
+
collaboratorIndicators.push({
|
|
152
|
+
color: presence.color,
|
|
153
|
+
shapeIds: visibleShapeIds,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
idsToDisplay,
|
|
160
|
+
renderingShapeIds,
|
|
161
|
+
hintingShapeIds,
|
|
162
|
+
collaboratorIndicators,
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ isEqual: isEqual },
|
|
166
|
+
[editor, activePeerIds$]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
useQuickReactor(
|
|
170
|
+
'canvas indicators render',
|
|
171
|
+
() => {
|
|
172
|
+
const canvas = canvasRef.current
|
|
173
|
+
if (!canvas) return
|
|
174
|
+
|
|
175
|
+
const ctx = canvas.getContext('2d')
|
|
176
|
+
if (!ctx) return
|
|
177
|
+
|
|
178
|
+
const { idsToDisplay, renderingShapeIds, hintingShapeIds, collaboratorIndicators } =
|
|
179
|
+
$renderData.get()
|
|
180
|
+
|
|
181
|
+
const { w, h } = editor.getViewportScreenBounds()
|
|
182
|
+
const dpr = window.devicePixelRatio || 1
|
|
183
|
+
const { x: cx, y: cy, z: zoom } = editor.getCamera()
|
|
184
|
+
|
|
185
|
+
const canvasWidth = Math.ceil(w * dpr)
|
|
186
|
+
const canvasHeight = Math.ceil(h * dpr)
|
|
187
|
+
|
|
188
|
+
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
|
189
|
+
canvas.width = canvasWidth
|
|
190
|
+
canvas.height = canvasHeight
|
|
191
|
+
canvas.style.width = `${w}px`
|
|
192
|
+
canvas.style.height = `${h}px`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ctx.resetTransform()
|
|
196
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
197
|
+
|
|
198
|
+
ctx.scale(dpr, dpr)
|
|
199
|
+
ctx.scale(zoom, zoom)
|
|
200
|
+
ctx.translate(cx, cy)
|
|
201
|
+
|
|
202
|
+
ctx.lineCap = 'round'
|
|
203
|
+
ctx.lineJoin = 'round'
|
|
204
|
+
|
|
205
|
+
// Draw collaborator indicators first (underneath local indicators)
|
|
206
|
+
// Use 0.5 opacity to match the original SVG-based collaborator indicators
|
|
207
|
+
ctx.lineWidth = 1.5 / zoom
|
|
208
|
+
for (const collaborator of collaboratorIndicators) {
|
|
209
|
+
ctx.strokeStyle = collaborator.color
|
|
210
|
+
ctx.globalAlpha = 0.7
|
|
211
|
+
for (const shapeId of collaborator.shapeIds) {
|
|
212
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Reset alpha for local indicators
|
|
217
|
+
ctx.globalAlpha = 1.0
|
|
218
|
+
|
|
219
|
+
// Use cached color, only call getComputedStyle when cache is empty
|
|
220
|
+
if (!rSelectedColor.current) {
|
|
221
|
+
rSelectedColor.current = getComputedStyle(canvas).getPropertyValue('--tl-color-selected')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ctx.strokeStyle = rSelectedColor.current
|
|
225
|
+
|
|
226
|
+
// Draw selected/hovered indicators (1.5px stroke)
|
|
227
|
+
ctx.lineWidth = 1.5 / zoom
|
|
228
|
+
for (const shapeId of idsToDisplay) {
|
|
229
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Draw hinted indicators with a thicker stroke (2.5px)
|
|
233
|
+
if (hintingShapeIds.length > 0) {
|
|
234
|
+
ctx.lineWidth = 2.5 / zoom
|
|
235
|
+
for (const shapeId of hintingShapeIds) {
|
|
236
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[editor, $renderData]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return <canvas ref={canvasRef} className="tl-canvas-indicators" />
|
|
244
|
+
})
|
|
@@ -26,6 +26,7 @@ import { GeometryDebuggingView } from '../GeometryDebuggingView'
|
|
|
26
26
|
import { LiveCollaborators } from '../LiveCollaborators'
|
|
27
27
|
import { MenuClickCapture } from '../MenuClickCapture'
|
|
28
28
|
import { Shape } from '../Shape'
|
|
29
|
+
import { CanvasShapeIndicators } from './CanvasShapeIndicators'
|
|
29
30
|
|
|
30
31
|
/** @public */
|
|
31
32
|
export interface TLCanvasComponentProps {
|
|
@@ -159,6 +160,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|
|
159
160
|
{hideShapes ? null : debugSvg ? <ShapesWithSVGs /> : <ShapesToDisplay />}
|
|
160
161
|
</div>
|
|
161
162
|
<div className="tl-overlays">
|
|
163
|
+
<CanvasShapeIndicators />
|
|
162
164
|
<div ref={rHtmlLayer2} className="tl-html-layer">
|
|
163
165
|
{debugGeometry ? <GeometryDebuggingView /> : null}
|
|
164
166
|
<BrushWrapper />
|
|
@@ -445,7 +447,19 @@ function HintedShapeIndicator() {
|
|
|
445
447
|
const editor = useEditor()
|
|
446
448
|
const { ShapeIndicator } = useEditorComponents()
|
|
447
449
|
|
|
448
|
-
const ids = useValue(
|
|
450
|
+
const ids = useValue(
|
|
451
|
+
'hinting shape ids without canvas indicator',
|
|
452
|
+
() => {
|
|
453
|
+
// Filter to only shapes that use legacy SVG indicators
|
|
454
|
+
return dedupe(editor.getHintingShapeIds()).filter((id) => {
|
|
455
|
+
const shape = editor.getShape(id)
|
|
456
|
+
if (!shape) return false
|
|
457
|
+
const util = editor.getShapeUtil(shape)
|
|
458
|
+
return util.useLegacyIndicator()
|
|
459
|
+
})
|
|
460
|
+
},
|
|
461
|
+
[editor]
|
|
462
|
+
)
|
|
449
463
|
|
|
450
464
|
if (!ids.length) return null
|
|
451
465
|
if (!ShapeIndicator) return null
|
|
@@ -32,6 +32,11 @@ const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId })
|
|
|
32
32
|
|
|
33
33
|
if (!shape || shape.isLocked) return null
|
|
34
34
|
|
|
35
|
+
const util = editor.getShapeUtil(shape)
|
|
36
|
+
|
|
37
|
+
// If the shape uses canvas indicators, it will be rendered by CanvasShapeIndicators
|
|
38
|
+
if (!util.useLegacyIndicator()) return null
|
|
39
|
+
|
|
35
40
|
return (
|
|
36
41
|
<OptionalErrorBoundary
|
|
37
42
|
fallback={ShapeIndicatorErrorFallback}
|
|
@@ -39,7 +44,7 @@ const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId })
|
|
|
39
44
|
editor.annotateError(error, { origin: 'react.shapeIndicator', willCrashApp: false })
|
|
40
45
|
}
|
|
41
46
|
>
|
|
42
|
-
<EvenInnererIndicator key={shape.id} shape={shape} util={
|
|
47
|
+
<EvenInnererIndicator key={shape.id} shape={shape} util={util} />
|
|
43
48
|
</OptionalErrorBoundary>
|
|
44
49
|
)
|
|
45
50
|
})
|
|
@@ -89,9 +89,24 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators({
|
|
|
89
89
|
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
|
|
90
90
|
|
|
91
91
|
const { ShapeIndicator } = useEditorComponents()
|
|
92
|
+
|
|
93
|
+
// Filter out shapes that have canvas indicator support - only render shapes that use legacy SVG indicators
|
|
94
|
+
const shapesToRender = useValue(
|
|
95
|
+
'shapes to render for svg indicators',
|
|
96
|
+
() => {
|
|
97
|
+
return renderingShapes.filter(({ id }) => {
|
|
98
|
+
const shape = editor.getShape(id)
|
|
99
|
+
if (!shape) return false
|
|
100
|
+
const util = editor.getShapeUtil(shape)
|
|
101
|
+
return util.useLegacyIndicator()
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
[editor, renderingShapes]
|
|
105
|
+
)
|
|
106
|
+
|
|
92
107
|
if (!ShapeIndicator) return null
|
|
93
108
|
|
|
94
|
-
return
|
|
109
|
+
return shapesToRender.map(({ id }) => (
|
|
95
110
|
<ShapeIndicator
|
|
96
111
|
key={id + '_indicator'}
|
|
97
112
|
shapeId={id}
|
|
@@ -26,6 +26,7 @@ export interface TLUserPreferences {
|
|
|
26
26
|
isPasteAtCursorMode?: boolean | null
|
|
27
27
|
enhancedA11yMode?: boolean | null
|
|
28
28
|
inputMode?: 'trackpad' | 'mouse' | null
|
|
29
|
+
isZoomDirectionInverted?: boolean | null
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
interface UserDataSnapshot {
|
|
@@ -56,6 +57,7 @@ export const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUser
|
|
|
56
57
|
isPasteAtCursorMode: T.boolean.nullable().optional(),
|
|
57
58
|
enhancedA11yMode: T.boolean.nullable().optional(),
|
|
58
59
|
inputMode: T.literalEnum('trackpad', 'mouse').nullable().optional(),
|
|
60
|
+
isZoomDirectionInverted: T.boolean.nullable().optional(),
|
|
59
61
|
})
|
|
60
62
|
|
|
61
63
|
const Versions = {
|
|
@@ -71,6 +73,7 @@ const Versions = {
|
|
|
71
73
|
AddShowUiLabels: 10,
|
|
72
74
|
AddPointerPeripheral: 11,
|
|
73
75
|
RenameShowUiLabelsToEnhancedA11yMode: 12,
|
|
76
|
+
AddZoomDirectionInverted: 13,
|
|
74
77
|
} as const
|
|
75
78
|
|
|
76
79
|
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
|
@@ -121,6 +124,10 @@ function migrateSnapshot(data: { version: number; user: any }) {
|
|
|
121
124
|
data.user.inputMode = null
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
if (data.version < Versions.AddZoomDirectionInverted) {
|
|
128
|
+
data.user.isZoomDirectionInverted = false
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
// finally
|
|
125
132
|
data.version = CURRENT_VERSION
|
|
126
133
|
}
|
|
@@ -171,6 +178,7 @@ export const defaultUserPreferences = Object.freeze({
|
|
|
171
178
|
enhancedA11yMode: false,
|
|
172
179
|
colorScheme: 'light',
|
|
173
180
|
inputMode: null,
|
|
181
|
+
isZoomDirectionInverted: false,
|
|
174
182
|
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
|
175
183
|
|
|
176
184
|
/** @public */
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -10544,7 +10544,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10544
10544
|
}
|
|
10545
10545
|
}
|
|
10546
10546
|
|
|
10547
|
-
|
|
10547
|
+
// because we can't for sure detect whether a user is using a mouse or a trackpad,
|
|
10548
|
+
// we need to check the input mode preference, and only invert the zoom direction
|
|
10549
|
+
// if the user has specifically set it to a mouse.
|
|
10550
|
+
const isZoomDirectionInverted =
|
|
10551
|
+
(this.user.getUserPreferences().isZoomDirectionInverted && inputMode === 'mouse') ??
|
|
10552
|
+
false
|
|
10553
|
+
const deltaValue = delta ?? 0
|
|
10554
|
+
const finalDelta = isZoomDirectionInverted ? -deltaValue : deltaValue
|
|
10555
|
+
|
|
10556
|
+
const zoom = cz + finalDelta * zoomSpeed * cz
|
|
10548
10557
|
this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), {
|
|
10549
10558
|
immediate: true,
|
|
10550
10559
|
})
|