@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.
Files changed (126) hide show
  1. package/dist-cjs/index.d.ts +277 -30
  2. package/dist-cjs/index.js +1 -6
  3. package/dist-cjs/index.js.map +3 -3
  4. package/dist-cjs/lib/TldrawEditor.js +20 -8
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
  7. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +12 -17
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +226 -0
  11. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
  12. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +45 -15
  13. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
  15. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
  17. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  18. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  19. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  20. package/dist-cjs/lib/config/createTLStore.js.map +1 -1
  21. package/dist-cjs/lib/editor/Editor.js +39 -18
  22. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  23. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +32 -13
  24. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +2 -3
  26. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
  28. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  29. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
  30. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  32. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  33. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
  34. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/editor/tools/RootState.js +0 -13
  36. package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePeerIds.js +35 -0
  38. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useShapeCulling.js +75 -0
  40. package/dist-cjs/lib/hooks/useShapeCulling.js.map +7 -0
  41. package/dist-cjs/lib/license/LicenseManager.js +6 -6
  42. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  43. package/dist-cjs/lib/options.js +7 -1
  44. package/dist-cjs/lib/options.js.map +2 -2
  45. package/dist-cjs/lib/utils/collaboratorState.js +42 -0
  46. package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
  47. package/dist-cjs/version.js +3 -3
  48. package/dist-cjs/version.js.map +1 -1
  49. package/dist-esm/index.d.mts +277 -30
  50. package/dist-esm/index.mjs +1 -6
  51. package/dist-esm/index.mjs.map +2 -2
  52. package/dist-esm/lib/TldrawEditor.mjs +20 -8
  53. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  54. package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
  55. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  56. package/dist-esm/lib/components/Shape.mjs +12 -17
  57. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +206 -0
  59. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +45 -15
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
  63. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
  65. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  66. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  67. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  68. package/dist-esm/lib/config/createTLStore.mjs.map +1 -1
  69. package/dist-esm/lib/editor/Editor.mjs +39 -18
  70. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  71. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +32 -13
  72. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  73. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +2 -3
  74. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  75. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
  76. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  77. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
  78. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  79. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  80. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  81. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
  82. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  83. package/dist-esm/lib/editor/tools/RootState.mjs +0 -13
  84. package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
  85. package/dist-esm/lib/hooks/usePeerIds.mjs +39 -1
  86. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  87. package/dist-esm/lib/hooks/useShapeCulling.mjs +55 -0
  88. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +7 -0
  89. package/dist-esm/lib/license/LicenseManager.mjs +6 -6
  90. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  91. package/dist-esm/lib/options.mjs +7 -1
  92. package/dist-esm/lib/options.mjs.map +2 -2
  93. package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
  94. package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
  95. package/dist-esm/version.mjs +3 -3
  96. package/dist-esm/version.mjs.map +1 -1
  97. package/editor.css +28 -11
  98. package/package.json +8 -11
  99. package/src/index.ts +2 -5
  100. package/src/lib/TldrawEditor.tsx +35 -13
  101. package/src/lib/components/LiveCollaborators.tsx +26 -37
  102. package/src/lib/components/Shape.tsx +15 -16
  103. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +288 -0
  104. package/src/lib/components/default-components/DefaultCanvas.tsx +39 -3
  105. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
  106. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
  107. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  108. package/src/lib/config/TLUserPreferences.ts +8 -0
  109. package/src/lib/config/createTLStore.ts +1 -1
  110. package/src/lib/editor/Editor.ts +72 -20
  111. package/src/lib/editor/derivations/notVisibleShapes.ts +39 -17
  112. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +0 -35
  113. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +4 -8
  114. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
  115. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
  116. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  117. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  118. package/src/lib/editor/shapes/ShapeUtil.ts +63 -5
  119. package/src/lib/editor/tools/RootState.ts +0 -16
  120. package/src/lib/hooks/usePeerIds.ts +53 -1
  121. package/src/lib/hooks/useShapeCulling.tsx +98 -0
  122. package/src/lib/license/LicenseManager.ts +6 -6
  123. package/src/lib/options.ts +48 -2
  124. package/src/lib/utils/collaboratorState.ts +54 -0
  125. package/src/version.ts +3 -3
  126. 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('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
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={editor.getShapeUtil(shape)} />
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 renderingShapes.map(({ id }) => (
109
+ return shapesToRender.map(({ id }) => (
95
110
  <ShapeIndicator
96
111
  key={id + '_indicator'}
97
112
  shapeId={id}
@@ -21,6 +21,7 @@ describe('TLUserPreferences consistency', () => {
21
21
  'isPasteAtCursorMode',
22
22
  'enhancedA11yMode',
23
23
  'inputMode',
24
+ 'isZoomDirectionInverted',
24
25
  ] as const
25
26
 
26
27
  it('defaultUserPreferences contains all TLUserPreferences keys (except 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