@tldraw/editor 4.3.0 → 4.4.0-canary.0791272556f5
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 +185 -16
- package/dist-cjs/index.js +3 -6
- package/dist-cjs/index.js.map +3 -3
- package/dist-cjs/lib/TldrawEditor.js.map +1 -1
- 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/config/createTLStore.js.map +1 -1
- package/dist-cjs/lib/editor/Editor.js +70 -16
- 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/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/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/editor/tools/RootState.js +0 -13
- package/dist-cjs/lib/editor/tools/RootState.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 +185 -16
- package/dist-esm/index.mjs +3 -6
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +1 -1
- 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/config/createTLStore.mjs.map +1 -1
- package/dist-esm/lib/editor/Editor.mjs +70 -16
- 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/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/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/editor/tools/RootState.mjs +0 -13
- package/dist-esm/lib/editor/tools/RootState.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 +11 -9
- package/package.json +10 -10
- package/src/index.ts +3 -5
- package/src/lib/TldrawEditor.tsx +1 -1
- 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/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +105 -17
- package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
- 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/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/editor/tools/RootState.ts +0 -16
- 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
|
@@ -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 */
|
|
@@ -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
|
|
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 })
|
|
@@ -439,6 +443,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
439
443
|
const deletedShapeIds = new Set<TLShapeId>()
|
|
440
444
|
const invalidParents = new Set<TLShapeId>()
|
|
441
445
|
let invalidBindingTypes = new Set<TLBinding['type']>()
|
|
446
|
+
|
|
442
447
|
this.disposables.add(
|
|
443
448
|
this.sideEffects.registerOperationCompleteHandler(() => {
|
|
444
449
|
// this needs to be cleared here because further effects may delete more shapes
|
|
@@ -895,6 +900,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
895
900
|
*/
|
|
896
901
|
readonly snaps: SnapManager
|
|
897
902
|
|
|
903
|
+
private readonly _spatialIndex: SpatialIndexManager
|
|
904
|
+
|
|
898
905
|
/**
|
|
899
906
|
* A manager for the any asynchronous events and making sure they're
|
|
900
907
|
* cleaned up upon disposal.
|
|
@@ -2766,7 +2773,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2766
2773
|
}
|
|
2767
2774
|
|
|
2768
2775
|
/**
|
|
2769
|
-
* Get the efficient zoom level. This returns the current zoom level if there are less than
|
|
2776
|
+
* Get the efficient zoom level. This returns the current zoom level if there are less than a certain number of shapes on the page,
|
|
2770
2777
|
* otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements.
|
|
2771
2778
|
*
|
|
2772
2779
|
* @public
|
|
@@ -3219,7 +3226,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3219
3226
|
* @public
|
|
3220
3227
|
*/
|
|
3221
3228
|
zoomToFit(opts?: TLCameraMoveOptions): this {
|
|
3222
|
-
const ids = [...this.getCurrentPageShapeIds()]
|
|
3229
|
+
const ids = [...this.getCurrentPageShapeIds()].filter((id) => !this.isShapeHidden(id))
|
|
3223
3230
|
if (ids.length <= 0) return this
|
|
3224
3231
|
const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
|
|
3225
3232
|
this.zoomToBounds(pageBounds, opts)
|
|
@@ -4174,23 +4181,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4174
4181
|
// unmount / remount in the DOM, which is expensive; and computing visibility is
|
|
4175
4182
|
// also expensive in large projects. For this reason, we use a second bounding
|
|
4176
4183
|
// box just for rendering, and we only update after the camera stops moving.
|
|
4177
|
-
private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
|
|
4178
4184
|
private _cameraStateTimeoutRemaining = 0
|
|
4179
4185
|
private _decayCameraStateTimeout(elapsed: number) {
|
|
4180
4186
|
this._cameraStateTimeoutRemaining -= elapsed
|
|
4181
4187
|
if (this._cameraStateTimeoutRemaining > 0) return
|
|
4182
4188
|
this.off('tick', this._decayCameraStateTimeout)
|
|
4183
|
-
this.
|
|
4189
|
+
this._setCameraState('idle')
|
|
4184
4190
|
}
|
|
4185
4191
|
private _tickCameraState() {
|
|
4186
4192
|
// always reset the timeout
|
|
4187
4193
|
this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
|
|
4188
4194
|
// If the state is idle, then start the tick
|
|
4189
|
-
if (this.
|
|
4190
|
-
this.
|
|
4195
|
+
if (this.getInstanceState().cameraState !== 'idle') return
|
|
4196
|
+
this._setCameraState('moving')
|
|
4191
4197
|
this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
|
|
4192
4198
|
this.on('tick', this._decayCameraStateTimeout)
|
|
4193
4199
|
}
|
|
4200
|
+
private _setCameraState(cameraState: 'idle' | 'moving') {
|
|
4201
|
+
this.updateInstanceState({ cameraState }, { history: 'ignore' })
|
|
4202
|
+
}
|
|
4194
4203
|
|
|
4195
4204
|
/**
|
|
4196
4205
|
* Whether the camera is moving or idle.
|
|
@@ -4203,7 +4212,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4203
4212
|
* @public
|
|
4204
4213
|
*/
|
|
4205
4214
|
getCameraState() {
|
|
4206
|
-
return this.
|
|
4215
|
+
return this.getInstanceState().cameraState
|
|
4207
4216
|
}
|
|
4208
4217
|
|
|
4209
4218
|
/**
|
|
@@ -5135,6 +5144,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5135
5144
|
}
|
|
5136
5145
|
|
|
5137
5146
|
private _notVisibleShapes = notVisibleShapes(this)
|
|
5147
|
+
private _culledShapesCache: Set<TLShapeId> | null = null
|
|
5138
5148
|
|
|
5139
5149
|
/**
|
|
5140
5150
|
* Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
|
|
@@ -5146,16 +5156,41 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5146
5156
|
const notVisibleShapes = this.getNotVisibleShapes()
|
|
5147
5157
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
5148
5158
|
const editingId = this.getEditingShapeId()
|
|
5149
|
-
const
|
|
5159
|
+
const nextValue = new Set<TLShapeId>(notVisibleShapes)
|
|
5150
5160
|
// we don't cull the shape we are editing
|
|
5151
5161
|
if (editingId) {
|
|
5152
|
-
|
|
5162
|
+
nextValue.delete(editingId)
|
|
5153
5163
|
}
|
|
5154
5164
|
// we also don't cull selected shapes
|
|
5155
5165
|
selectedShapeIds.forEach((id) => {
|
|
5156
|
-
|
|
5166
|
+
nextValue.delete(id)
|
|
5157
5167
|
})
|
|
5158
|
-
|
|
5168
|
+
|
|
5169
|
+
// Cache optimization: return same Set object if contents unchanged
|
|
5170
|
+
// This allows consumers to use === comparison and prevents unnecessary re-renders
|
|
5171
|
+
const prevValue = this._culledShapesCache
|
|
5172
|
+
if (prevValue) {
|
|
5173
|
+
// If sizes differ, contents must differ
|
|
5174
|
+
if (prevValue.size !== nextValue.size) {
|
|
5175
|
+
this._culledShapesCache = nextValue
|
|
5176
|
+
return nextValue
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
// Check if all elements are the same
|
|
5180
|
+
for (const id of prevValue) {
|
|
5181
|
+
if (!nextValue.has(id)) {
|
|
5182
|
+
// Found a difference, update cache and return new set
|
|
5183
|
+
this._culledShapesCache = nextValue
|
|
5184
|
+
return nextValue
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
// Loop completed without finding differences - contents identical
|
|
5189
|
+
return prevValue
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
this._culledShapesCache = nextValue
|
|
5193
|
+
return nextValue
|
|
5159
5194
|
}
|
|
5160
5195
|
|
|
5161
5196
|
/**
|
|
@@ -5167,6 +5202,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5167
5202
|
let commonBounds: Box | undefined
|
|
5168
5203
|
|
|
5169
5204
|
this.getCurrentPageShapeIdsSorted().forEach((shapeId) => {
|
|
5205
|
+
if (this.isShapeHidden(shapeId)) return
|
|
5170
5206
|
const bounds = this.getShapeMaskedPageBounds(shapeId)
|
|
5171
5207
|
if (!bounds) return
|
|
5172
5208
|
if (!commonBounds) {
|
|
@@ -5222,11 +5258,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5222
5258
|
let inMarginClosestToEdgeDistance = Infinity
|
|
5223
5259
|
let inMarginClosestToEdgeHit: TLShape | null = null
|
|
5224
5260
|
|
|
5261
|
+
// Use larger margin for spatial search to account for edge distance checks
|
|
5262
|
+
const searchMargin = Math.max(innerMargin, outerMargin, this.options.hitTestMargin / zoomLevel)
|
|
5263
|
+
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
|
|
5264
|
+
|
|
5225
5265
|
const shapesToCheck = (
|
|
5226
5266
|
opts.renderingOnly
|
|
5227
5267
|
? this.getCurrentPageRenderingShapesSorted()
|
|
5228
5268
|
: this.getCurrentPageShapesSorted()
|
|
5229
5269
|
).filter((shape) => {
|
|
5270
|
+
// Frames have labels positioned above the shape (outside bounds), so always include them
|
|
5271
|
+
if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
|
|
5272
|
+
|
|
5230
5273
|
if (
|
|
5231
5274
|
(shape.isLocked && !hitLocked) ||
|
|
5232
5275
|
this.isShapeHidden(shape) ||
|
|
@@ -5412,11 +5455,41 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5412
5455
|
point: VecLike,
|
|
5413
5456
|
opts = {} as { margin?: number; hitInside?: boolean }
|
|
5414
5457
|
): TLShape[] {
|
|
5458
|
+
const margin = opts.margin ?? 0
|
|
5459
|
+
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
|
|
5460
|
+
|
|
5461
|
+
// Get all page shapes in z-index order and filter to candidates that pass isPointInShape
|
|
5462
|
+
// Frames are always checked because their labels can be outside their bounds
|
|
5415
5463
|
return this.getCurrentPageShapesSorted()
|
|
5416
|
-
.filter((shape) =>
|
|
5464
|
+
.filter((shape) => {
|
|
5465
|
+
if (this.isShapeHidden(shape)) return false
|
|
5466
|
+
if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false
|
|
5467
|
+
return this.isPointInShape(shape, point, opts)
|
|
5468
|
+
})
|
|
5417
5469
|
.reverse()
|
|
5418
5470
|
}
|
|
5419
5471
|
|
|
5472
|
+
/**
|
|
5473
|
+
* Get shape IDs within the given bounds.
|
|
5474
|
+
*
|
|
5475
|
+
* Note: Uses shape page bounds only. Frames with labels outside their bounds
|
|
5476
|
+
* may not be included even if the label is within the search bounds.
|
|
5477
|
+
*
|
|
5478
|
+
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
|
|
5479
|
+
* ```ts
|
|
5480
|
+
* const candidates = editor.getShapeIdsInsideBounds(bounds)
|
|
5481
|
+
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
|
|
5482
|
+
* ```
|
|
5483
|
+
*
|
|
5484
|
+
* @param bounds - The bounds to search within.
|
|
5485
|
+
* @returns Unordered set of shape IDs within the given bounds.
|
|
5486
|
+
*
|
|
5487
|
+
* @internal
|
|
5488
|
+
*/
|
|
5489
|
+
getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
|
|
5490
|
+
return this._spatialIndex.getShapeIdsInsideBounds(bounds)
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5420
5493
|
/**
|
|
5421
5494
|
* Test whether a point (in the current page space) will will a shape. This method takes into account masks,
|
|
5422
5495
|
* such as when a shape is the child of a frame and is partially clipped by the frame.
|
|
@@ -9892,7 +9965,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9892
9965
|
/**
|
|
9893
9966
|
* Handles navigating to the content specified by the query param in the given URL.
|
|
9894
9967
|
*
|
|
9895
|
-
* Use {@link Editor
|
|
9968
|
+
* Use {@link Editor.createDeepLink} to create a URL with a deep link query param.
|
|
9896
9969
|
*
|
|
9897
9970
|
* If no URL is provided, it will look for the param in the current `window.location.href`.
|
|
9898
9971
|
*
|
|
@@ -10351,9 +10424,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10351
10424
|
if (inputs.getIsPinching()) return
|
|
10352
10425
|
|
|
10353
10426
|
if (!inputs.getIsEditing()) {
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10427
|
+
// Always capture the current selection when pinch starts.
|
|
10428
|
+
// This ensures Safari (which uses gesture events instead of wheel)
|
|
10429
|
+
// doesn't restore a stale selection from an earlier pointer_down.
|
|
10430
|
+
this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
|
|
10357
10431
|
|
|
10358
10432
|
this._didPinch = true
|
|
10359
10433
|
|
|
@@ -10475,7 +10549,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10475
10549
|
}
|
|
10476
10550
|
}
|
|
10477
10551
|
|
|
10478
|
-
|
|
10552
|
+
// because we can't for sure detect whether a user is using a mouse or a trackpad,
|
|
10553
|
+
// we need to check the input mode preference, and only invert the zoom direction
|
|
10554
|
+
// if the user has specifically set it to a mouse.
|
|
10555
|
+
const isZoomDirectionInverted =
|
|
10556
|
+
(this.user.getUserPreferences().isZoomDirectionInverted && inputMode === 'mouse') ??
|
|
10557
|
+
false
|
|
10558
|
+
const deltaValue = delta ?? 0
|
|
10559
|
+
const finalDelta = isZoomDirectionInverted ? -deltaValue : deltaValue
|
|
10560
|
+
|
|
10561
|
+
const zoom = cz + finalDelta * zoomSpeed * cz
|
|
10479
10562
|
this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), {
|
|
10480
10563
|
immediate: true,
|
|
10481
10564
|
})
|
|
@@ -10655,6 +10738,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10655
10738
|
this.setCurrentTool(this._restoreToolId)
|
|
10656
10739
|
}
|
|
10657
10740
|
}
|
|
10741
|
+
|
|
10742
|
+
// Clear the stashed selection so the next pinch captures fresh state.
|
|
10743
|
+
// This fixes Safari pinch zoom restoring outdated selections.
|
|
10744
|
+
this._selectedShapeIdsAtPointerDown = []
|
|
10745
|
+
|
|
10658
10746
|
break
|
|
10659
10747
|
}
|
|
10660
10748
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
|
316
|
-
redos: redos
|
|
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
|
-
|
|
354
|
+
type Stack<T> = StackItem<T> | EmptyStackItem<T>
|
|
355
355
|
|
|
356
|
-
|
|
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>
|
|
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>
|
|
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
|
-
|
|
407
|
-
|
|
386
|
+
function stackToArray<T>(stack: Stack<T>) {
|
|
387
|
+
if (!stack.length) {
|
|
388
|
+
return EMPTY_ARRAY
|
|
408
389
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
}
|