@tldraw/editor 4.4.0-next.f181afb0ab39 → 4.4.0-next.f2df7e20ec7b

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 (86) hide show
  1. package/dist-cjs/index.d.ts +102 -19
  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/Shape.js +12 -17
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +26 -1
  9. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +16 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/config/createTLStore.js.map +1 -1
  13. package/dist-cjs/lib/editor/Editor.js +35 -17
  14. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  15. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +32 -13
  16. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +2 -3
  18. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +13 -38
  20. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  21. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  22. package/dist-cjs/lib/editor/tools/RootState.js +0 -13
  23. package/dist-cjs/lib/editor/tools/RootState.js.map +2 -2
  24. package/dist-cjs/lib/hooks/usePeerIds.js +8 -2
  25. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  26. package/dist-cjs/lib/hooks/useShapeCulling.js +75 -0
  27. package/dist-cjs/lib/hooks/useShapeCulling.js.map +7 -0
  28. package/dist-cjs/lib/license/LicenseManager.js +6 -6
  29. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  30. package/dist-cjs/lib/options.js +6 -1
  31. package/dist-cjs/lib/options.js.map +2 -2
  32. package/dist-cjs/version.js +3 -3
  33. package/dist-cjs/version.js.map +1 -1
  34. package/dist-esm/index.d.mts +102 -19
  35. package/dist-esm/index.mjs +1 -6
  36. package/dist-esm/index.mjs.map +2 -2
  37. package/dist-esm/lib/TldrawEditor.mjs +20 -8
  38. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  39. package/dist-esm/lib/components/Shape.mjs +12 -17
  40. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  41. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +27 -2
  42. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
  43. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +16 -1
  44. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  45. package/dist-esm/lib/config/createTLStore.mjs.map +1 -1
  46. package/dist-esm/lib/editor/Editor.mjs +35 -17
  47. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  48. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +32 -13
  49. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  50. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +2 -3
  51. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  52. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +14 -39
  53. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  54. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  55. package/dist-esm/lib/editor/tools/RootState.mjs +0 -13
  56. package/dist-esm/lib/editor/tools/RootState.mjs.map +2 -2
  57. package/dist-esm/lib/hooks/usePeerIds.mjs +8 -2
  58. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  59. package/dist-esm/lib/hooks/useShapeCulling.mjs +55 -0
  60. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +7 -0
  61. package/dist-esm/lib/license/LicenseManager.mjs +6 -6
  62. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  63. package/dist-esm/lib/options.mjs +6 -1
  64. package/dist-esm/lib/options.mjs.map +2 -2
  65. package/dist-esm/version.mjs +3 -3
  66. package/dist-esm/version.mjs.map +1 -1
  67. package/editor.css +22 -11
  68. package/package.json +8 -11
  69. package/src/index.ts +0 -5
  70. package/src/lib/TldrawEditor.tsx +35 -13
  71. package/src/lib/components/Shape.tsx +15 -16
  72. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +46 -2
  73. package/src/lib/components/default-components/DefaultCanvas.tsx +24 -2
  74. package/src/lib/config/createTLStore.ts +1 -1
  75. package/src/lib/editor/Editor.ts +62 -19
  76. package/src/lib/editor/derivations/notVisibleShapes.ts +39 -17
  77. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +0 -35
  78. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +4 -8
  79. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +19 -47
  80. package/src/lib/editor/shapes/ShapeUtil.ts +19 -5
  81. package/src/lib/editor/tools/RootState.ts +0 -16
  82. package/src/lib/hooks/usePeerIds.ts +9 -2
  83. package/src/lib/hooks/useShapeCulling.tsx +98 -0
  84. package/src/lib/license/LicenseManager.ts +6 -6
  85. package/src/lib/options.ts +41 -2
  86. package/src/version.ts +3 -3
@@ -5,6 +5,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
5
5
  import { ShapeUtil } from '../editor/shapes/ShapeUtil'
6
6
  import { useEditor } from '../hooks/useEditor'
7
7
  import { useEditorComponents } from '../hooks/useEditorComponents'
8
+ import { useShapeCulling } from '../hooks/useShapeCulling'
8
9
  import { Mat } from '../primitives/Mat'
9
10
  import { areShapesContentEqual } from '../utils/areShapesContentEqual'
10
11
  import { setStyleProperty } from '../utils/dom'
@@ -57,7 +58,6 @@ export const Shape = memo(function Shape({
57
58
  height: 0,
58
59
  x: 0,
59
60
  y: 0,
60
- isCulled: false,
61
61
  })
62
62
 
63
63
  useQuickReactor(
@@ -118,22 +118,21 @@ export const Shape = memo(function Shape({
118
118
  setStyleProperty(bgContainer, 'z-index', backgroundIndex)
119
119
  }, [opacity, index, backgroundIndex])
120
120
 
121
- useQuickReactor(
122
- 'set display',
123
- () => {
124
- const shape = editor.getShape(id)
125
- if (!shape) return // probably the shape was just deleted
121
+ // Register container refs with the centralized culling context.
122
+ // This runs on mount and handles initial display state.
123
+ const { register, unregister } = useShapeCulling()
124
+ useLayoutEffect(() => {
125
+ const container = containerRef.current
126
+ if (!container) return
126
127
 
127
- const culledShapes = editor.getCulledShapes()
128
- const isCulled = culledShapes.has(id)
129
- if (isCulled !== memoizedStuffRef.current.isCulled) {
130
- setStyleProperty(containerRef.current, 'display', isCulled ? 'none' : 'block')
131
- setStyleProperty(bgContainerRef.current, 'display', isCulled ? 'none' : 'block')
132
- memoizedStuffRef.current.isCulled = isCulled
133
- }
134
- },
135
- [editor]
136
- )
128
+ // Check initial culling state and register with the context
129
+ const isCulled = editor.getCulledShapes().has(id)
130
+ register(id, container, bgContainerRef.current, isCulled)
131
+
132
+ return () => {
133
+ unregister(id)
134
+ }
135
+ }, [editor, id, register, unregister])
137
136
  const annotateError = useCallback(
138
137
  (error: any) => editor.annotateError(error, { origin: 'shape', willCrashApp: false }),
139
138
  [editor]
@@ -1,7 +1,7 @@
1
1
  import { useComputed, useQuickReactor } from '@tldraw/state-react'
2
2
  import { createComputedCache } from '@tldraw/store'
3
3
  import { TLShape, TLShapeId } from '@tldraw/tlschema'
4
- import { dedupe, isEqual } from '@tldraw/utils'
4
+ import { dedupe } from '@tldraw/utils'
5
5
  import { memo, useEffect, useRef } from 'react'
6
6
  import { Editor } from '../../editor/Editor'
7
7
  import { TLIndicatorPath } from '../../editor/shapes/ShapeUtil'
@@ -14,6 +14,50 @@ interface CollaboratorIndicatorData {
14
14
  shapeIds: TLShapeId[]
15
15
  }
16
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
+
17
61
  const indicatorPathCache = createComputedCache(
18
62
  'indicatorPath',
19
63
  (editor: Editor, shape: TLShape) => {
@@ -162,7 +206,7 @@ export const CanvasShapeIndicators = memo(function CanvasShapeIndicators() {
162
206
  collaboratorIndicators,
163
207
  }
164
208
  },
165
- { isEqual: isEqual },
209
+ { isEqual: renderDataEqual },
166
210
  [editor, activePeerIds$]
167
211
  )
168
212
 
@@ -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'
@@ -428,18 +429,39 @@ function ReflowIfNeeded() {
428
429
  return null
429
430
  }
430
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
+
431
452
  function ShapesToDisplay() {
432
453
  const editor = useEditor()
433
454
 
434
455
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
435
456
 
436
457
  return (
437
- <>
458
+ <ShapeCullingProvider>
438
459
  {renderingShapes.map((result) => (
439
460
  <Shape key={result.id + '_shape'} {...result} />
440
461
  ))}
462
+ <CullingController />
441
463
  {tlenv.isSafari && <ReflowIfNeeded />}
442
- </>
464
+ </ShapeCullingProvider>
443
465
  )
444
466
  }
445
467
 
@@ -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
 
@@ -153,7 +153,13 @@ import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndex
153
153
  import { TextManager } from './managers/TextManager/TextManager'
154
154
  import { TickManager } from './managers/TickManager/TickManager'
155
155
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
156
- import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
156
+ import {
157
+ ShapeUtil,
158
+ TLEditStartInfo,
159
+ TLGeometryOpts,
160
+ TLResizeMode,
161
+ TLShapeUtilCanBindOpts,
162
+ } from './shapes/ShapeUtil'
157
163
  import { RootState } from './tools/RootState'
158
164
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
159
165
  import { TLContent } from './types/clipboard-types'
@@ -228,10 +234,17 @@ export interface TLEditorOptions {
228
234
  inferDarkMode?: boolean
229
235
  /**
230
236
  * Options for the editor's camera.
237
+ *
238
+ * @deprecated Use `options.cameraOptions` instead. This will be removed in a future release.
231
239
  */
232
240
  cameraOptions?: Partial<TLCameraOptions>
233
- textOptions?: TLTextOptions
234
241
  options?: Partial<TldrawOptions>
242
+ /**
243
+ * Text options for the editor.
244
+ *
245
+ * @deprecated Use `options.text` instead. This prop will be removed in a future release.
246
+ */
247
+ textOptions?: TLTextOptions
235
248
  licenseKey?: string
236
249
  fontAssetUrls?: { [key: string]: string | undefined }
237
250
  /**
@@ -283,12 +296,16 @@ export class Editor extends EventEmitter<TLEventMap> {
283
296
  bindingUtils,
284
297
  tools,
285
298
  getContainer,
299
+ // needs to be here for backwards compatibility with TldrawEditor
300
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
286
301
  cameraOptions,
287
- textOptions,
288
302
  initialState,
289
303
  autoFocus,
290
304
  inferDarkMode,
291
- options,
305
+ options: _options,
306
+ // needs to be here for backwards compatibility with TldrawEditor
307
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
308
+ textOptions: _textOptions,
292
309
  getShapeVisibility,
293
310
  fontAssetUrls,
294
311
  }: TLEditorOptions) {
@@ -296,6 +313,10 @@ export class Editor extends EventEmitter<TLEventMap> {
296
313
 
297
314
  this._getShapeVisibility = getShapeVisibility
298
315
 
316
+ // Merge deprecated textOptions prop with options.text
317
+ // options.text takes precedence over the deprecated textOptions prop
318
+ const options = _textOptions ? { ..._options, text: _options?.text ?? _textOptions } : _options
319
+
299
320
  this.options = { ...defaultTldrawOptions, ...options }
300
321
 
301
322
  this.store = store
@@ -314,9 +335,14 @@ export class Editor extends EventEmitter<TLEventMap> {
314
335
 
315
336
  this.disposables.add(this.timers.dispose)
316
337
 
317
- this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
338
+ // Merge camera options: options.cameraOptions takes precedence over deprecated cameraOptions prop
339
+ this._cameraOptions.set({
340
+ ...DEFAULT_CAMERA_OPTIONS,
341
+ ...cameraOptions,
342
+ ...options?.camera,
343
+ })
318
344
 
319
- this._textOptions = atom('text options', textOptions ?? null)
345
+ this._textOptions = atom('text options', options?.text ?? null)
320
346
 
321
347
  this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
322
348
  this.disposables.add(() => this.user.dispose())
@@ -443,6 +469,7 @@ export class Editor extends EventEmitter<TLEventMap> {
443
469
  const deletedShapeIds = new Set<TLShapeId>()
444
470
  const invalidParents = new Set<TLShapeId>()
445
471
  let invalidBindingTypes = new Set<TLBinding['type']>()
472
+
446
473
  this.disposables.add(
447
474
  this.sideEffects.registerOperationCompleteHandler(() => {
448
475
  // this needs to be cleared here because further effects may delete more shapes
@@ -2772,7 +2799,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2772
2799
  }
2773
2800
 
2774
2801
  /**
2775
- * Get the efficient zoom level. This returns the current zoom level if there are less than 300 shapes on the page,
2802
+ * Get the efficient zoom level. This returns the current zoom level if there are less than a certain number of shapes on the page,
2776
2803
  * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
2777
2804
  *
2778
2805
  * @public
@@ -3225,7 +3252,7 @@ export class Editor extends EventEmitter<TLEventMap> {
3225
3252
  * @public
3226
3253
  */
3227
3254
  zoomToFit(opts?: TLCameraMoveOptions): this {
3228
- const ids = [...this.getCurrentPageShapeIds()]
3255
+ const ids = [...this.getCurrentPageShapeIds()].filter((id) => !this.isShapeHidden(id))
3229
3256
  if (ids.length <= 0) return this
3230
3257
  const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
3231
3258
  this.zoomToBounds(pageBounds, opts)
@@ -4180,23 +4207,25 @@ export class Editor extends EventEmitter<TLEventMap> {
4180
4207
  // unmount / remount in the DOM, which is expensive; and computing visibility is
4181
4208
  // also expensive in large projects. For this reason, we use a second bounding
4182
4209
  // box just for rendering, and we only update after the camera stops moving.
4183
- private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4184
4210
  private _cameraStateTimeoutRemaining = 0
4185
4211
  private _decayCameraStateTimeout(elapsed: number) {
4186
4212
  this._cameraStateTimeoutRemaining -= elapsed
4187
4213
  if (this._cameraStateTimeoutRemaining > 0) return
4188
4214
  this.off('tick', this._decayCameraStateTimeout)
4189
- this._cameraState.set('idle')
4215
+ this._setCameraState('idle')
4190
4216
  }
4191
4217
  private _tickCameraState() {
4192
4218
  // always reset the timeout
4193
4219
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4194
4220
  // If the state is idle, then start the tick
4195
- if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4196
- this._cameraState.set('moving')
4221
+ if (this.getInstanceState().cameraState !== 'idle') return
4222
+ this._setCameraState('moving')
4197
4223
  this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4198
4224
  this.on('tick', this._decayCameraStateTimeout)
4199
4225
  }
4226
+ private _setCameraState(cameraState: 'idle' | 'moving') {
4227
+ this.updateInstanceState({ cameraState }, { history: 'ignore' })
4228
+ }
4200
4229
 
4201
4230
  /**
4202
4231
  * Whether the camera is moving or idle.
@@ -4209,7 +4238,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4209
4238
  * @public
4210
4239
  */
4211
4240
  getCameraState() {
4212
- return this._cameraState.get()
4241
+ return this.getInstanceState().cameraState
4213
4242
  }
4214
4243
 
4215
4244
  /**
@@ -5199,6 +5228,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5199
5228
  let commonBounds: Box | undefined
5200
5229
 
5201
5230
  this.getCurrentPageShapeIdsSorted().forEach((shapeId) => {
5231
+ if (this.isShapeHidden(shapeId)) return
5202
5232
  const bounds = this.getShapeMaskedPageBounds(shapeId)
5203
5233
  if (!bounds) return
5204
5234
  if (!commonBounds) {
@@ -5480,7 +5510,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5480
5510
  * @param bounds - The bounds to search within.
5481
5511
  * @returns Unordered set of shape IDs within the given bounds.
5482
5512
  *
5483
- * @internal
5513
+ * @public
5484
5514
  */
5485
5515
  getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
5486
5516
  return this._spatialIndex.getShapeIdsInsideBounds(bounds)
@@ -6245,7 +6275,13 @@ export class Editor extends EventEmitter<TLEventMap> {
6245
6275
  const toShapeType = typeof toShape === 'string' ? toShape : toShape.type
6246
6276
  const bindingType = typeof binding === 'string' ? binding : binding.type
6247
6277
 
6248
- const canBindOpts = { fromShapeType, toShapeType, bindingType } as const
6278
+ const canBindOpts: TLShapeUtilCanBindOpts = {
6279
+ fromShape: typeof fromShape === 'string' ? { type: fromShape } : fromShape,
6280
+ toShape: typeof toShape === 'string' ? { type: toShape } : toShape,
6281
+ bindingType,
6282
+ fromShapeType,
6283
+ toShapeType,
6284
+ }
6249
6285
 
6250
6286
  if (fromShapeType === toShapeType) {
6251
6287
  return this.getShapeUtil(fromShapeType).canBind(canBindOpts)
@@ -7838,6 +7874,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7838
7874
  initialShape: options.initialShape,
7839
7875
  initialBounds: options.initialBounds,
7840
7876
  isAspectRatioLocked: options.isAspectRatioLocked,
7877
+ initialPageTransform: options.initialPageTransform,
7841
7878
  })
7842
7879
 
7843
7880
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
@@ -9961,7 +9998,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9961
9998
  /**
9962
9999
  * Handles navigating to the content specified by the query param in the given URL.
9963
10000
  *
9964
- * Use {@link Editor#createDeepLink} to create a URL with a deep link query param.
10001
+ * Use {@link Editor.createDeepLink} to create a URL with a deep link query param.
9965
10002
  *
9966
10003
  * If no URL is provided, it will look for the param in the current `window.location.href`.
9967
10004
  *
@@ -10420,9 +10457,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10420
10457
  if (inputs.getIsPinching()) return
10421
10458
 
10422
10459
  if (!inputs.getIsEditing()) {
10423
- if (!this._selectedShapeIdsAtPointerDown.length) {
10424
- this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10425
- }
10460
+ // Always capture the current selection when pinch starts.
10461
+ // This ensures Safari (which uses gesture events instead of wheel)
10462
+ // doesn't restore a stale selection from an earlier pointer_down.
10463
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10426
10464
 
10427
10465
  this._didPinch = true
10428
10466
 
@@ -10733,6 +10771,11 @@ export class Editor extends EventEmitter<TLEventMap> {
10733
10771
  this.setCurrentTool(this._restoreToolId)
10734
10772
  }
10735
10773
  }
10774
+
10775
+ // Clear the stashed selection so the next pinch captures fresh state.
10776
+ // This fixes Safari pinch zoom restoring outdated selections.
10777
+ this._selectedShapeIdsAtPointerDown = []
10778
+
10736
10779
  break
10737
10780
  }
10738
10781
  }
@@ -1,5 +1,5 @@
1
1
  import { computed, isUninitialized } from '@tldraw/state'
2
- import { TLShapeId } from '@tldraw/tlschema'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
3
  import { Editor } from '../Editor'
4
4
 
5
5
  /**
@@ -9,34 +9,56 @@ import { Editor } from '../Editor'
9
9
  * @returns Incremental derivation of non visible shapes.
10
10
  */
11
11
  export function notVisibleShapes(editor: Editor) {
12
+ const emptySet = new Set<TLShapeId>()
13
+
12
14
  return computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {
13
- const allShapeIds = editor.getCurrentPageShapeIds()
15
+ const allShapes = editor.getCurrentPageShapes()
14
16
  const viewportPageBounds = editor.getViewportPageBounds()
15
17
  const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)
16
18
 
17
- const nextValue = new Set<TLShapeId>()
18
-
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
19
+ let shape: TLShape | undefined
24
20
 
25
- const canCull = editor.getShapeUtil(shape.type).canCull(shape)
26
- if (!canCull) continue
27
-
28
- nextValue.add(id)
21
+ // Fast path: if all shapes are visible, return empty set
22
+ if (visibleIds.size === allShapes.length) {
23
+ if (isUninitialized(prevValue) || prevValue.size > 0) {
24
+ return emptySet
29
25
  }
26
+ return prevValue
30
27
  }
31
28
 
32
- if (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {
29
+ // First run: compute from scratch
30
+ if (isUninitialized(prevValue)) {
31
+ const nextValue = new Set<TLShapeId>()
32
+ for (let i = 0; i < allShapes.length; i++) {
33
+ shape = allShapes[i]
34
+ if (visibleIds.has(shape.id)) continue
35
+ if (!editor.getShapeUtil(shape.type).canCull(shape)) continue
36
+ nextValue.add(shape.id)
37
+ }
33
38
  return nextValue
34
39
  }
35
40
 
36
- for (const prev of prevValue) {
37
- if (!nextValue.has(prev)) return nextValue
41
+ // Subsequent runs: single pass to collect IDs and detect changes
42
+ const notVisibleIds: TLShapeId[] = []
43
+ for (let i = 0; i < allShapes.length; i++) {
44
+ shape = allShapes[i]
45
+ if (visibleIds.has(shape.id)) continue
46
+ if (!editor.getShapeUtil(shape.type).canCull(shape)) continue
47
+ notVisibleIds.push(shape.id)
48
+ }
49
+
50
+ // Check if the result changed
51
+ if (notVisibleIds.length === prevValue.size) {
52
+ let same = true
53
+ for (let i = 0; i < notVisibleIds.length; i++) {
54
+ if (!prevValue.has(notVisibleIds[i])) {
55
+ same = false
56
+ break
57
+ }
58
+ }
59
+ if (same) return prevValue
38
60
  }
39
61
 
40
- return prevValue
62
+ return new Set(notVisibleIds)
41
63
  })
42
64
  }
@@ -240,41 +240,6 @@ describe('EdgeScrollManager', () => {
240
240
  })
241
241
  })
242
242
 
243
- describe('camera movement conditions', () => {
244
- it('should not move camera when not dragging', () => {
245
- editor.inputs.setIsDragging(false)
246
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
247
-
248
- edgeScrollManager.updateEdgeScrolling(300)
249
-
250
- expect(editor.setCamera).not.toHaveBeenCalled()
251
- })
252
-
253
- it('should not move camera when panning', () => {
254
- editor.inputs.setIsPanning(true)
255
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
256
-
257
- edgeScrollManager.updateEdgeScrolling(300)
258
-
259
- expect(editor.setCamera).not.toHaveBeenCalled()
260
- })
261
-
262
- it('should not move camera when camera is locked', () => {
263
- editor.getCameraOptions.mockReturnValue({
264
- isLocked: true,
265
- panSpeed: 1,
266
- zoomSpeed: 1,
267
- zoomSteps: [1],
268
- wheelBehavior: 'pan' as const,
269
- })
270
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
271
-
272
- edgeScrollManager.updateEdgeScrolling(300)
273
-
274
- expect(editor.setCamera).not.toHaveBeenCalled()
275
- })
276
- })
277
-
278
243
  describe('camera movement calculation', () => {
279
244
  it('should calculate scroll speed based on user preference', () => {
280
245
  editor.user.getEdgeScrollSpeed.mockReturnValue(2)
@@ -21,6 +21,9 @@ export class EdgeScrollManager {
21
21
  */
22
22
  updateEdgeScrolling(elapsed: number) {
23
23
  const { editor } = this
24
+
25
+ if (editor.getCameraOptions().isLocked) return
26
+
24
27
  const edgeScrollProximityFactor = this.getEdgeScroll()
25
28
  if (edgeScrollProximityFactor.x === 0 && edgeScrollProximityFactor.y === 0) {
26
29
  if (this._isEdgeScrolling) {
@@ -106,15 +109,8 @@ export class EdgeScrollManager {
106
109
  * @public
107
110
  */
108
111
  private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
109
- const { editor } = this
110
- if (
111
- !editor.inputs.getIsDragging() ||
112
- editor.inputs.getIsPanning() ||
113
- editor.getCameraOptions().isLocked
114
- )
115
- return
116
-
117
112
  if (proximityFactor.x === 0 && proximityFactor.y === 0) return
113
+ const { editor } = this
118
114
 
119
115
  const screenBounds = editor.getViewportScreenBounds()
120
116
 
@@ -1,12 +1,12 @@
1
- import { atom, transact } from '@tldraw/state'
1
+ import { atom, EMPTY_ARRAY, transact } from '@tldraw/state'
2
2
  import {
3
- RecordsDiff,
4
- Store,
5
- UnknownRecord,
6
3
  createEmptyRecordsDiff,
7
4
  isRecordsDiffEmpty,
5
+ RecordsDiff,
8
6
  reverseRecordsDiff,
9
7
  squashRecordDiffsMutable,
8
+ Store,
9
+ UnknownRecord,
10
10
  } from '@tldraw/store'
11
11
  import { exhaustiveSwitchError, noop } from '@tldraw/utils'
12
12
  import { TLHistoryBatchOptions, TLHistoryEntry } from '../../types/history-types'
@@ -312,8 +312,8 @@ export class HistoryManager<R extends UnknownRecord> {
312
312
  debug() {
313
313
  const { undos, redos } = this.stacks.get()
314
314
  return {
315
- undos: undos.toArray(),
316
- redos: redos.toArray(),
315
+ undos: stackToArray(undos),
316
+ redos: stackToArray(redos),
317
317
  pendingDiff: this.pendingDiff.debug(),
318
318
  state: this.state as string,
319
319
  }
@@ -351,22 +351,13 @@ class PendingDiff<R extends UnknownRecord> {
351
351
  }
352
352
  }
353
353
 
354
- import { EMPTY_ARRAY } from '@tldraw/state'
354
+ type Stack<T> = StackItem<T> | EmptyStackItem<T>
355
355
 
356
- export type Stack<T> = StackItem<T> | EmptyStackItem<T>
357
-
358
- export function stack<T>(items?: Array<T>): Stack<T> {
359
- if (items) {
360
- let result = EMPTY_STACK_ITEM as Stack<T>
361
- while (items.length) {
362
- result = result.push(items.pop()!)
363
- }
364
- return result
365
- }
356
+ function stack<T>(): Stack<T> {
366
357
  return EMPTY_STACK_ITEM as any
367
358
  }
368
359
 
369
- class EmptyStackItem<T> implements Iterable<T> {
360
+ class EmptyStackItem<T> {
370
361
  readonly length = 0
371
362
  readonly head = null
372
363
  readonly tail: Stack<T> = this
@@ -374,23 +365,11 @@ class EmptyStackItem<T> implements Iterable<T> {
374
365
  push(head: T): Stack<T> {
375
366
  return new StackItem<T>(head, this)
376
367
  }
377
-
378
- toArray() {
379
- return EMPTY_ARRAY
380
- }
381
-
382
- [Symbol.iterator]() {
383
- return {
384
- next() {
385
- return { value: undefined, done: true as const }
386
- },
387
- }
388
- }
389
368
  }
390
369
 
391
370
  const EMPTY_STACK_ITEM = new EmptyStackItem()
392
371
 
393
- class StackItem<T> implements Iterable<T> {
372
+ class StackItem<T> {
394
373
  length: number
395
374
  constructor(
396
375
  public readonly head: T,
@@ -402,23 +381,16 @@ class StackItem<T> implements Iterable<T> {
402
381
  push(head: T): Stack<T> {
403
382
  return new StackItem(head, this)
404
383
  }
384
+ }
405
385
 
406
- toArray() {
407
- return Array.from(this)
386
+ function stackToArray<T>(stack: Stack<T>) {
387
+ if (!stack.length) {
388
+ return EMPTY_ARRAY
408
389
  }
409
-
410
- [Symbol.iterator]() {
411
- let stack = this as Stack<T>
412
- return {
413
- next() {
414
- if (stack.length) {
415
- const value = stack.head!
416
- stack = stack.tail
417
- return { value, done: false as const }
418
- } else {
419
- return { value: undefined, done: true as const }
420
- }
421
- },
422
- }
390
+ const arr: T[] = []
391
+ while (stack.length) {
392
+ arr.push(stack.head!)
393
+ stack = stack.tail
423
394
  }
395
+ return arr
424
396
  }