@tldraw/editor 4.3.0 → 4.4.0-canary.09e80a09d230
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 +1 -1
- package/dist-cjs/index.d.ts +180 -11
- package/dist-cjs/index.js +3 -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 +30 -16
- 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 +58 -6
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.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/RBushIndex.js +144 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
- 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 +180 -11
- package/dist-esm/index.mjs +3 -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 +30 -16
- 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 +58 -6
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
- package/dist-esm/lib/editor/derivations/notVisibleShapes.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/RBushIndex.mjs +114 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
- 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 +10 -8
- package/src/index.ts +3 -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 +16 -6
- 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 +84 -6
- package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
- package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
- 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
|
@@ -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 />
|
|
@@ -412,11 +414,7 @@ function ReflowIfNeeded() {
|
|
|
412
414
|
'reflow for culled shapes',
|
|
413
415
|
() => {
|
|
414
416
|
const culledShapes = editor.getCulledShapes()
|
|
415
|
-
if (
|
|
416
|
-
culledShapesRef.current.size === culledShapes.size &&
|
|
417
|
-
[...culledShapes].every((id) => culledShapesRef.current.has(id))
|
|
418
|
-
)
|
|
419
|
-
return
|
|
417
|
+
if (culledShapesRef.current === culledShapes) return
|
|
420
418
|
|
|
421
419
|
culledShapesRef.current = culledShapes
|
|
422
420
|
const canvas = document.getElementsByClassName('tl-canvas')
|
|
@@ -449,7 +447,19 @@ function HintedShapeIndicator() {
|
|
|
449
447
|
const editor = useEditor()
|
|
450
448
|
const { ShapeIndicator } = useEditorComponents()
|
|
451
449
|
|
|
452
|
-
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
|
+
)
|
|
453
463
|
|
|
454
464
|
if (!ids.length) return null
|
|
455
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
|
@@ -149,6 +149,7 @@ import { HistoryManager } from './managers/HistoryManager/HistoryManager'
|
|
|
149
149
|
import { InputsManager } from './managers/InputsManager/InputsManager'
|
|
150
150
|
import { ScribbleManager } from './managers/ScribbleManager/ScribbleManager'
|
|
151
151
|
import { SnapManager } from './managers/SnapManager/SnapManager'
|
|
152
|
+
import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndexManager'
|
|
152
153
|
import { TextManager } from './managers/TextManager/TextManager'
|
|
153
154
|
import { TickManager } from './managers/TickManager/TickManager'
|
|
154
155
|
import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
|
|
@@ -308,6 +309,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
308
309
|
|
|
309
310
|
this.snaps = new SnapManager(this)
|
|
310
311
|
|
|
312
|
+
this._spatialIndex = new SpatialIndexManager(this)
|
|
313
|
+
this.disposables.add(() => this._spatialIndex.dispose())
|
|
314
|
+
|
|
311
315
|
this.disposables.add(this.timers.dispose)
|
|
312
316
|
|
|
313
317
|
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
|
@@ -895,6 +899,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
895
899
|
*/
|
|
896
900
|
readonly snaps: SnapManager
|
|
897
901
|
|
|
902
|
+
private readonly _spatialIndex: SpatialIndexManager
|
|
903
|
+
|
|
898
904
|
/**
|
|
899
905
|
* A manager for the any asynchronous events and making sure they're
|
|
900
906
|
* cleaned up upon disposal.
|
|
@@ -5135,6 +5141,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5135
5141
|
}
|
|
5136
5142
|
|
|
5137
5143
|
private _notVisibleShapes = notVisibleShapes(this)
|
|
5144
|
+
private _culledShapesCache: Set<TLShapeId> | null = null
|
|
5138
5145
|
|
|
5139
5146
|
/**
|
|
5140
5147
|
* Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
|
|
@@ -5146,16 +5153,41 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5146
5153
|
const notVisibleShapes = this.getNotVisibleShapes()
|
|
5147
5154
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
5148
5155
|
const editingId = this.getEditingShapeId()
|
|
5149
|
-
const
|
|
5156
|
+
const nextValue = new Set<TLShapeId>(notVisibleShapes)
|
|
5150
5157
|
// we don't cull the shape we are editing
|
|
5151
5158
|
if (editingId) {
|
|
5152
|
-
|
|
5159
|
+
nextValue.delete(editingId)
|
|
5153
5160
|
}
|
|
5154
5161
|
// we also don't cull selected shapes
|
|
5155
5162
|
selectedShapeIds.forEach((id) => {
|
|
5156
|
-
|
|
5163
|
+
nextValue.delete(id)
|
|
5157
5164
|
})
|
|
5158
|
-
|
|
5165
|
+
|
|
5166
|
+
// Cache optimization: return same Set object if contents unchanged
|
|
5167
|
+
// This allows consumers to use === comparison and prevents unnecessary re-renders
|
|
5168
|
+
const prevValue = this._culledShapesCache
|
|
5169
|
+
if (prevValue) {
|
|
5170
|
+
// If sizes differ, contents must differ
|
|
5171
|
+
if (prevValue.size !== nextValue.size) {
|
|
5172
|
+
this._culledShapesCache = nextValue
|
|
5173
|
+
return nextValue
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
// Check if all elements are the same
|
|
5177
|
+
for (const id of prevValue) {
|
|
5178
|
+
if (!nextValue.has(id)) {
|
|
5179
|
+
// Found a difference, update cache and return new set
|
|
5180
|
+
this._culledShapesCache = nextValue
|
|
5181
|
+
return nextValue
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
// Loop completed without finding differences - contents identical
|
|
5186
|
+
return prevValue
|
|
5187
|
+
}
|
|
5188
|
+
|
|
5189
|
+
this._culledShapesCache = nextValue
|
|
5190
|
+
return nextValue
|
|
5159
5191
|
}
|
|
5160
5192
|
|
|
5161
5193
|
/**
|
|
@@ -5222,11 +5254,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5222
5254
|
let inMarginClosestToEdgeDistance = Infinity
|
|
5223
5255
|
let inMarginClosestToEdgeHit: TLShape | null = null
|
|
5224
5256
|
|
|
5257
|
+
// Use larger margin for spatial search to account for edge distance checks
|
|
5258
|
+
const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
|
|
5259
|
+
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
|
|
5260
|
+
|
|
5225
5261
|
const shapesToCheck = (
|
|
5226
5262
|
opts.renderingOnly
|
|
5227
5263
|
? this.getCurrentPageRenderingShapesSorted()
|
|
5228
5264
|
: this.getCurrentPageShapesSorted()
|
|
5229
5265
|
).filter((shape) => {
|
|
5266
|
+
// Frames have labels positioned above the shape (outside bounds), so always include them
|
|
5267
|
+
if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
|
|
5268
|
+
|
|
5230
5269
|
if (
|
|
5231
5270
|
(shape.isLocked && !hitLocked) ||
|
|
5232
5271
|
this.isShapeHidden(shape) ||
|
|
@@ -5412,11 +5451,41 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5412
5451
|
point: VecLike,
|
|
5413
5452
|
opts = {} as { margin?: number; hitInside?: boolean }
|
|
5414
5453
|
): TLShape[] {
|
|
5454
|
+
const margin = opts.margin ?? 0
|
|
5455
|
+
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
|
|
5456
|
+
|
|
5457
|
+
// Get all page shapes in z-index order and filter to candidates that pass isPointInShape
|
|
5458
|
+
// Frames are always checked because their labels can be outside their bounds
|
|
5415
5459
|
return this.getCurrentPageShapesSorted()
|
|
5416
|
-
.filter((shape) =>
|
|
5460
|
+
.filter((shape) => {
|
|
5461
|
+
if (this.isShapeHidden(shape)) return false
|
|
5462
|
+
if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
|
|
5463
|
+
return this.isPointInShape(shape, point, opts)
|
|
5464
|
+
})
|
|
5417
5465
|
.reverse()
|
|
5418
5466
|
}
|
|
5419
5467
|
|
|
5468
|
+
/**
|
|
5469
|
+
* Get shape IDs within the given bounds.
|
|
5470
|
+
*
|
|
5471
|
+
* Note: Uses shape page bounds only. Frames with labels outside their bounds
|
|
5472
|
+
* may not be included even if the label is within the search bounds.
|
|
5473
|
+
*
|
|
5474
|
+
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
|
|
5475
|
+
* ```ts
|
|
5476
|
+
* const candidates = editor.getShapeIdsInsideBounds(bounds)
|
|
5477
|
+
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
|
|
5478
|
+
* ```
|
|
5479
|
+
*
|
|
5480
|
+
* @param bounds - The bounds to search within.
|
|
5481
|
+
* @returns Unordered set of shape IDs within the given bounds.
|
|
5482
|
+
*
|
|
5483
|
+
* @internal
|
|
5484
|
+
*/
|
|
5485
|
+
getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
|
|
5486
|
+
return this._spatialIndex.getShapeIdsInsideBounds(bounds)
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5420
5489
|
/**
|
|
5421
5490
|
* Test whether a point (in the current page space) will will a shape. This method takes into account masks,
|
|
5422
5491
|
* such as when a shape is the child of a frame and is partially clipped by the frame.
|
|
@@ -10475,7 +10544,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10475
10544
|
}
|
|
10476
10545
|
}
|
|
10477
10546
|
|
|
10478
|
-
|
|
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
|
|
10479
10557
|
this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), {
|
|
10480
10558
|
immediate: true,
|
|
10481
10559
|
})
|
|
@@ -9,60 +9,34 @@ import { Editor } from '../Editor'
|
|
|
9
9
|
* @returns Incremental derivation of non visible shapes.
|
|
10
10
|
*/
|
|
11
11
|
export function notVisibleShapes(editor: Editor) {
|
|
12
|
-
return computed<Set<TLShapeId>>('notVisibleShapes', function
|
|
13
|
-
const
|
|
14
|
-
const nextValue = new Set<TLShapeId>()
|
|
15
|
-
|
|
16
|
-
// Extract viewport bounds once to avoid repeated property access
|
|
12
|
+
return computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {
|
|
13
|
+
const allShapeIds = editor.getCurrentPageShapeIds()
|
|
17
14
|
const viewportPageBounds = editor.getViewportPageBounds()
|
|
18
|
-
const
|
|
19
|
-
const viewMinY = viewportPageBounds.minY
|
|
20
|
-
const viewMaxX = viewportPageBounds.maxX
|
|
21
|
-
const viewMaxY = viewportPageBounds.maxY
|
|
15
|
+
const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
const pageBounds = editor.getShapePageBounds(id)
|
|
25
|
-
|
|
26
|
-
// Hybrid check: if bounds exist and shape overlaps viewport, it's visible.
|
|
27
|
-
// This inlines Box.Collides to avoid function call overhead and the
|
|
28
|
-
// redundant Contains check that Box.Includes was doing.
|
|
29
|
-
if (
|
|
30
|
-
pageBounds !== undefined &&
|
|
31
|
-
pageBounds.maxX >= viewMinX &&
|
|
32
|
-
pageBounds.minX <= viewMaxX &&
|
|
33
|
-
pageBounds.maxY >= viewMinY &&
|
|
34
|
-
pageBounds.minY <= viewMaxY
|
|
35
|
-
) {
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
17
|
+
const nextValue = new Set<TLShapeId>()
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
19
|
+
// Non-visible shapes are all shapes minus visible shapes
|
|
20
|
+
for (const id of allShapeIds) {
|
|
21
|
+
if (!visibleIds.has(id)) {
|
|
22
|
+
const shape = editor.getShape(id)
|
|
23
|
+
if (!shape) continue
|
|
44
24
|
|
|
45
|
-
|
|
46
|
-
|
|
25
|
+
const canCull = editor.getShapeUtil(shape.type).canCull(shape)
|
|
26
|
+
if (!canCull) continue
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
nextValue.add(id)
|
|
29
|
+
}
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
if (isUninitialized(prevValue)) {
|
|
32
|
+
if (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {
|
|
52
33
|
return nextValue
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
// If there are more or less shapes, we know there's a change
|
|
56
|
-
if (prevValue.size !== nextValue.size) return nextValue
|
|
57
|
-
|
|
58
|
-
// If any of the old shapes are not in the new set, we know there's a change
|
|
59
36
|
for (const prev of prevValue) {
|
|
60
|
-
if (!nextValue.has(prev))
|
|
61
|
-
return nextValue
|
|
62
|
-
}
|
|
37
|
+
if (!nextValue.has(prev)) return nextValue
|
|
63
38
|
}
|
|
64
39
|
|
|
65
|
-
// If we've made it here, we know that the set is the same
|
|
66
40
|
return prevValue
|
|
67
41
|
})
|
|
68
42
|
}
|