@tldraw/editor 4.4.0-next.84d68f44c848 → 4.4.0-next.bde73a32273d
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 +277 -30
- package/dist-cjs/index.js +1 -6
- package/dist-cjs/index.js.map +3 -3
- package/dist-cjs/lib/TldrawEditor.js +20 -8
- package/dist-cjs/lib/TldrawEditor.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/Shape.js +12 -17
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +226 -0
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +45 -15
- 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/config/createTLStore.js.map +1 -1
- package/dist-cjs/lib/editor/Editor.js +39 -18
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +32 -13
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +2 -3
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
- 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/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/editor/tools/RootState.js +0 -13
- package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +35 -0
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/hooks/useShapeCulling.js +75 -0
- package/dist-cjs/lib/hooks/useShapeCulling.js.map +7 -0
- package/dist-cjs/lib/license/LicenseManager.js +6 -6
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/options.js +7 -1
- 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 +277 -30
- package/dist-esm/index.mjs +1 -6
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +20 -8
- package/dist-esm/lib/TldrawEditor.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/Shape.mjs +12 -17
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +206 -0
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +45 -15
- 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/config/createTLStore.mjs.map +1 -1
- package/dist-esm/lib/editor/Editor.mjs +39 -18
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +32 -13
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +2 -3
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.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/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/editor/tools/RootState.mjs +0 -13
- package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +39 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/hooks/useShapeCulling.mjs +55 -0
- package/dist-esm/lib/hooks/useShapeCulling.mjs.map +7 -0
- package/dist-esm/lib/license/LicenseManager.mjs +6 -6
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +7 -1
- 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 +28 -11
- package/package.json +8 -11
- package/src/index.ts +2 -5
- package/src/lib/TldrawEditor.tsx +35 -13
- package/src/lib/components/LiveCollaborators.tsx +26 -37
- package/src/lib/components/Shape.tsx +15 -16
- package/src/lib/components/default-components/CanvasShapeIndicators.tsx +288 -0
- package/src/lib/components/default-components/DefaultCanvas.tsx +39 -3
- 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/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +72 -20
- package/src/lib/editor/derivations/notVisibleShapes.ts +39 -17
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +0 -35
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +4 -8
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
- 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 +63 -5
- package/src/lib/editor/tools/RootState.ts +0 -16
- package/src/lib/hooks/usePeerIds.ts +53 -1
- package/src/lib/hooks/useShapeCulling.tsx +98 -0
- package/src/lib/license/LicenseManager.ts +6 -6
- package/src/lib/options.ts +48 -2
- 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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { useComputed, useQuickReactor } from '@tldraw/state-react'
|
|
2
|
+
import { createComputedCache } from '@tldraw/store'
|
|
3
|
+
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
4
|
+
import { dedupe } 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
|
+
interface RenderData {
|
|
18
|
+
idsToDisplay: Set<TLShapeId>
|
|
19
|
+
renderingShapeIds: Set<TLShapeId>
|
|
20
|
+
hintingShapeIds: TLShapeId[]
|
|
21
|
+
collaboratorIndicators: CollaboratorIndicatorData[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
|
25
|
+
if (a.size !== b.size) return false
|
|
26
|
+
for (const item of a) {
|
|
27
|
+
if (!b.has(item)) return false
|
|
28
|
+
}
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function arraysEqual<T>(a: readonly T[], b: readonly T[]): boolean {
|
|
33
|
+
if (a.length !== b.length) return false
|
|
34
|
+
for (let i = 0; i < a.length; i++) {
|
|
35
|
+
if (a[i] !== b[i]) return false
|
|
36
|
+
}
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collaboratorIndicatorsEqual(
|
|
41
|
+
a: CollaboratorIndicatorData[],
|
|
42
|
+
b: CollaboratorIndicatorData[]
|
|
43
|
+
): boolean {
|
|
44
|
+
if (a.length !== b.length) return false
|
|
45
|
+
for (let i = 0; i < a.length; i++) {
|
|
46
|
+
if (a[i].color !== b[i].color) return false
|
|
47
|
+
if (!arraysEqual(a[i].shapeIds, b[i].shapeIds)) return false
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderDataEqual(a: RenderData, b: RenderData): boolean {
|
|
53
|
+
return (
|
|
54
|
+
setsEqual(a.idsToDisplay, b.idsToDisplay) &&
|
|
55
|
+
setsEqual(a.renderingShapeIds, b.renderingShapeIds) &&
|
|
56
|
+
arraysEqual(a.hintingShapeIds, b.hintingShapeIds) &&
|
|
57
|
+
collaboratorIndicatorsEqual(a.collaboratorIndicators, b.collaboratorIndicators)
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const indicatorPathCache = createComputedCache(
|
|
62
|
+
'indicatorPath',
|
|
63
|
+
(editor: Editor, shape: TLShape) => {
|
|
64
|
+
const util = editor.getShapeUtil(shape)
|
|
65
|
+
return util.getIndicatorPath(shape)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const getIndicatorPath = (editor: Editor, shape: TLShape) => {
|
|
70
|
+
return indicatorPathCache.get(editor, shape.id)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderShapeIndicator(
|
|
74
|
+
ctx: CanvasRenderingContext2D,
|
|
75
|
+
editor: Editor,
|
|
76
|
+
shapeId: TLShapeId,
|
|
77
|
+
renderingShapeIds: Set<TLShapeId>
|
|
78
|
+
): boolean {
|
|
79
|
+
if (!renderingShapeIds.has(shapeId)) return false
|
|
80
|
+
|
|
81
|
+
const shape = editor.getShape(shapeId)
|
|
82
|
+
if (!shape || shape.isLocked) return false
|
|
83
|
+
|
|
84
|
+
const pageTransform = editor.getShapePageTransform(shape)
|
|
85
|
+
if (!pageTransform) return false
|
|
86
|
+
|
|
87
|
+
const indicatorPath = getIndicatorPath(editor, shape)
|
|
88
|
+
if (!indicatorPath) return false
|
|
89
|
+
|
|
90
|
+
ctx.save()
|
|
91
|
+
ctx.transform(
|
|
92
|
+
pageTransform.a,
|
|
93
|
+
pageTransform.b,
|
|
94
|
+
pageTransform.c,
|
|
95
|
+
pageTransform.d,
|
|
96
|
+
pageTransform.e,
|
|
97
|
+
pageTransform.f
|
|
98
|
+
)
|
|
99
|
+
renderIndicatorPath(ctx, indicatorPath)
|
|
100
|
+
ctx.restore()
|
|
101
|
+
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderIndicatorPath(ctx: CanvasRenderingContext2D, indicatorPath: TLIndicatorPath) {
|
|
106
|
+
if (indicatorPath instanceof Path2D) {
|
|
107
|
+
ctx.stroke(indicatorPath)
|
|
108
|
+
} else {
|
|
109
|
+
const { path, clipPath, additionalPaths } = indicatorPath
|
|
110
|
+
|
|
111
|
+
if (clipPath) {
|
|
112
|
+
ctx.save()
|
|
113
|
+
ctx.clip(clipPath, 'evenodd')
|
|
114
|
+
ctx.stroke(path)
|
|
115
|
+
ctx.restore()
|
|
116
|
+
} else {
|
|
117
|
+
ctx.stroke(path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (additionalPaths) {
|
|
121
|
+
for (const additionalPath of additionalPaths) {
|
|
122
|
+
ctx.stroke(additionalPath)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @internal @react */
|
|
129
|
+
export const CanvasShapeIndicators = memo(function CanvasShapeIndicators() {
|
|
130
|
+
const editor = useEditor()
|
|
131
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
132
|
+
|
|
133
|
+
// Cache the selected color to avoid getComputedStyle on every render
|
|
134
|
+
const rSelectedColor = useRef<string | null>(null)
|
|
135
|
+
const isDarkMode = useIsDarkMode()
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const timer = editor.timers.setTimeout(() => {
|
|
139
|
+
rSelectedColor.current = null
|
|
140
|
+
}, 0)
|
|
141
|
+
return () => clearTimeout(timer)
|
|
142
|
+
}, [isDarkMode, editor])
|
|
143
|
+
|
|
144
|
+
// Get active peer IDs (already handles time-based state transitions)
|
|
145
|
+
const activePeerIds$ = useActivePeerIds$()
|
|
146
|
+
|
|
147
|
+
const $renderData = useComputed(
|
|
148
|
+
'indicator render data',
|
|
149
|
+
() => {
|
|
150
|
+
const renderingShapeIds = new Set(editor.getRenderingShapes().map((s) => s.id))
|
|
151
|
+
|
|
152
|
+
// Compute ids to display for selected/hovered shapes
|
|
153
|
+
const idsToDisplay = new Set<TLShapeId>()
|
|
154
|
+
const instanceState = editor.getInstanceState()
|
|
155
|
+
const isChangingStyle = instanceState.isChangingStyle
|
|
156
|
+
const isIdleOrEditing = editor.isInAny('select.idle', 'select.editing_shape')
|
|
157
|
+
const isInSelectState = editor.isInAny(
|
|
158
|
+
'select.brushing',
|
|
159
|
+
'select.scribble_brushing',
|
|
160
|
+
'select.pointing_shape',
|
|
161
|
+
'select.pointing_selection',
|
|
162
|
+
'select.pointing_handle'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (!isChangingStyle && (isIdleOrEditing || isInSelectState)) {
|
|
166
|
+
for (const id of editor.getSelectedShapeIds()) {
|
|
167
|
+
idsToDisplay.add(id)
|
|
168
|
+
}
|
|
169
|
+
if (isIdleOrEditing && instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
|
|
170
|
+
const hovered = editor.getHoveredShapeId()
|
|
171
|
+
if (hovered) idsToDisplay.add(hovered)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compute hinting shape ids
|
|
176
|
+
const hintingShapeIds = dedupe(editor.getHintingShapeIds())
|
|
177
|
+
|
|
178
|
+
// Compute collaborator indicators
|
|
179
|
+
const collaboratorIndicators: CollaboratorIndicatorData[] = []
|
|
180
|
+
const currentPageId = editor.getCurrentPageId()
|
|
181
|
+
const activePeerIds = activePeerIds$.get()
|
|
182
|
+
|
|
183
|
+
const collaborators = editor.getCollaborators()
|
|
184
|
+
for (const peerId of activePeerIds.values()) {
|
|
185
|
+
// Skip collaborators on different pages
|
|
186
|
+
const presence = collaborators.find((c) => c.userId === peerId)
|
|
187
|
+
if (!presence || presence.currentPageId !== currentPageId) continue
|
|
188
|
+
|
|
189
|
+
// Filter to shapes that are visible and on the current rendering set
|
|
190
|
+
const visibleShapeIds = presence.selectedShapeIds.filter(
|
|
191
|
+
(id) => renderingShapeIds.has(id) && !editor.isShapeHidden(id)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (visibleShapeIds.length > 0) {
|
|
195
|
+
collaboratorIndicators.push({
|
|
196
|
+
color: presence.color,
|
|
197
|
+
shapeIds: visibleShapeIds,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
idsToDisplay,
|
|
204
|
+
renderingShapeIds,
|
|
205
|
+
hintingShapeIds,
|
|
206
|
+
collaboratorIndicators,
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{ isEqual: renderDataEqual },
|
|
210
|
+
[editor, activePeerIds$]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
useQuickReactor(
|
|
214
|
+
'canvas indicators render',
|
|
215
|
+
() => {
|
|
216
|
+
const canvas = canvasRef.current
|
|
217
|
+
if (!canvas) return
|
|
218
|
+
|
|
219
|
+
const ctx = canvas.getContext('2d')
|
|
220
|
+
if (!ctx) return
|
|
221
|
+
|
|
222
|
+
const { idsToDisplay, renderingShapeIds, hintingShapeIds, collaboratorIndicators } =
|
|
223
|
+
$renderData.get()
|
|
224
|
+
|
|
225
|
+
const { w, h } = editor.getViewportScreenBounds()
|
|
226
|
+
const dpr = window.devicePixelRatio || 1
|
|
227
|
+
const { x: cx, y: cy, z: zoom } = editor.getCamera()
|
|
228
|
+
|
|
229
|
+
const canvasWidth = Math.ceil(w * dpr)
|
|
230
|
+
const canvasHeight = Math.ceil(h * dpr)
|
|
231
|
+
|
|
232
|
+
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
|
233
|
+
canvas.width = canvasWidth
|
|
234
|
+
canvas.height = canvasHeight
|
|
235
|
+
canvas.style.width = `${w}px`
|
|
236
|
+
canvas.style.height = `${h}px`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ctx.resetTransform()
|
|
240
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
241
|
+
|
|
242
|
+
ctx.scale(dpr, dpr)
|
|
243
|
+
ctx.scale(zoom, zoom)
|
|
244
|
+
ctx.translate(cx, cy)
|
|
245
|
+
|
|
246
|
+
ctx.lineCap = 'round'
|
|
247
|
+
ctx.lineJoin = 'round'
|
|
248
|
+
|
|
249
|
+
// Draw collaborator indicators first (underneath local indicators)
|
|
250
|
+
// Use 0.5 opacity to match the original SVG-based collaborator indicators
|
|
251
|
+
ctx.lineWidth = 1.5 / zoom
|
|
252
|
+
for (const collaborator of collaboratorIndicators) {
|
|
253
|
+
ctx.strokeStyle = collaborator.color
|
|
254
|
+
ctx.globalAlpha = 0.7
|
|
255
|
+
for (const shapeId of collaborator.shapeIds) {
|
|
256
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Reset alpha for local indicators
|
|
261
|
+
ctx.globalAlpha = 1.0
|
|
262
|
+
|
|
263
|
+
// Use cached color, only call getComputedStyle when cache is empty
|
|
264
|
+
if (!rSelectedColor.current) {
|
|
265
|
+
rSelectedColor.current = getComputedStyle(canvas).getPropertyValue('--tl-color-selected')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
ctx.strokeStyle = rSelectedColor.current
|
|
269
|
+
|
|
270
|
+
// Draw selected/hovered indicators (1.5px stroke)
|
|
271
|
+
ctx.lineWidth = 1.5 / zoom
|
|
272
|
+
for (const shapeId of idsToDisplay) {
|
|
273
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Draw hinted indicators with a thicker stroke (2.5px)
|
|
277
|
+
if (hintingShapeIds.length > 0) {
|
|
278
|
+
ctx.lineWidth = 2.5 / zoom
|
|
279
|
+
for (const shapeId of hintingShapeIds) {
|
|
280
|
+
renderShapeIndicator(ctx, editor, shapeId, renderingShapeIds)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
[editor, $renderData]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return <canvas ref={canvasRef} className="tl-canvas-indicators" />
|
|
288
|
+
})
|
|
@@ -16,6 +16,7 @@ import { useGestureEvents } from '../../hooks/useGestureEvents'
|
|
|
16
16
|
import { useHandleEvents } from '../../hooks/useHandleEvents'
|
|
17
17
|
import { useSharedSafeId } from '../../hooks/useSafeId'
|
|
18
18
|
import { useScreenBounds } from '../../hooks/useScreenBounds'
|
|
19
|
+
import { ShapeCullingProvider, useShapeCulling } from '../../hooks/useShapeCulling'
|
|
19
20
|
import { Box } from '../../primitives/Box'
|
|
20
21
|
import { Mat } from '../../primitives/Mat'
|
|
21
22
|
import { Vec } from '../../primitives/Vec'
|
|
@@ -26,6 +27,7 @@ import { GeometryDebuggingView } from '../GeometryDebuggingView'
|
|
|
26
27
|
import { LiveCollaborators } from '../LiveCollaborators'
|
|
27
28
|
import { MenuClickCapture } from '../MenuClickCapture'
|
|
28
29
|
import { Shape } from '../Shape'
|
|
30
|
+
import { CanvasShapeIndicators } from './CanvasShapeIndicators'
|
|
29
31
|
|
|
30
32
|
/** @public */
|
|
31
33
|
export interface TLCanvasComponentProps {
|
|
@@ -159,6 +161,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|
|
159
161
|
{hideShapes ? null : debugSvg ? <ShapesWithSVGs /> : <ShapesToDisplay />}
|
|
160
162
|
</div>
|
|
161
163
|
<div className="tl-overlays">
|
|
164
|
+
<CanvasShapeIndicators />
|
|
162
165
|
<div ref={rHtmlLayer2} className="tl-html-layer">
|
|
163
166
|
{debugGeometry ? <GeometryDebuggingView /> : null}
|
|
164
167
|
<BrushWrapper />
|
|
@@ -426,18 +429,39 @@ function ReflowIfNeeded() {
|
|
|
426
429
|
return null
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Centralized culling controller that updates shape container visibility.
|
|
434
|
+
* This single reactor replaces per-shape subscriptions for O(1) instead of O(N) subscriptions.
|
|
435
|
+
*/
|
|
436
|
+
function CullingController() {
|
|
437
|
+
const editor = useEditor()
|
|
438
|
+
const { updateCulling } = useShapeCulling()
|
|
439
|
+
|
|
440
|
+
useQuickReactor(
|
|
441
|
+
'update shape culling',
|
|
442
|
+
() => {
|
|
443
|
+
const culledShapes = editor.getCulledShapes()
|
|
444
|
+
updateCulling(culledShapes)
|
|
445
|
+
},
|
|
446
|
+
[editor, updateCulling]
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
|
|
429
452
|
function ShapesToDisplay() {
|
|
430
453
|
const editor = useEditor()
|
|
431
454
|
|
|
432
455
|
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
|
|
433
456
|
|
|
434
457
|
return (
|
|
435
|
-
|
|
458
|
+
<ShapeCullingProvider>
|
|
436
459
|
{renderingShapes.map((result) => (
|
|
437
460
|
<Shape key={result.id + '_shape'} {...result} />
|
|
438
461
|
))}
|
|
462
|
+
<CullingController />
|
|
439
463
|
{tlenv.isSafari && <ReflowIfNeeded />}
|
|
440
|
-
|
|
464
|
+
</ShapeCullingProvider>
|
|
441
465
|
)
|
|
442
466
|
}
|
|
443
467
|
|
|
@@ -445,7 +469,19 @@ function HintedShapeIndicator() {
|
|
|
445
469
|
const editor = useEditor()
|
|
446
470
|
const { ShapeIndicator } = useEditorComponents()
|
|
447
471
|
|
|
448
|
-
const ids = useValue(
|
|
472
|
+
const ids = useValue(
|
|
473
|
+
'hinting shape ids without canvas indicator',
|
|
474
|
+
() => {
|
|
475
|
+
// Filter to only shapes that use legacy SVG indicators
|
|
476
|
+
return dedupe(editor.getHintingShapeIds()).filter((id) => {
|
|
477
|
+
const shape = editor.getShape(id)
|
|
478
|
+
if (!shape) return false
|
|
479
|
+
const util = editor.getShapeUtil(shape)
|
|
480
|
+
return util.useLegacyIndicator()
|
|
481
|
+
})
|
|
482
|
+
},
|
|
483
|
+
[editor]
|
|
484
|
+
)
|
|
449
485
|
|
|
450
486
|
if (!ids.length) return null
|
|
451
487
|
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 */
|
|
@@ -29,7 +29,7 @@ export interface TLStoreBaseOptions {
|
|
|
29
29
|
/** How should this store upload & resolve assets? */
|
|
30
30
|
assets?: TLAssetStore
|
|
31
31
|
|
|
32
|
-
/** Called when the store is connected to an {@link Editor}. */
|
|
32
|
+
/** Called when the store is connected to an {@link @tldraw/editor#Editor}. */
|
|
33
33
|
onMount?(editor: Editor): void | (() => void)
|
|
34
34
|
}
|
|
35
35
|
|