2024-04-16 10:56:35 +00:00
|
|
|
import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
|
Perf: Incremental culled shapes calculation. (#3411)
Reworks our culling logic:
- No longer show the gray rectangles for culled shapes.
- Don't use `renderingBoundExpanded`, instead we now use
`viewportPageBounds`. I've removed `renderingBoundsExpanded`, but we
might want to deprecate it?
- There's now a incremental computation of non visible shapes, which are
shapes outside of `viewportPageBounds` and shapes that outside of their
parents' clipping bounds.
- There's also a new `getCulledShapes` function in `Editor`, which uses
the non visible shapes computation as a part of the culled shape
computation.
- Also moved some of the `getRenderingShapes` tests to newly created
`getCullingShapes` tests.
Feels much better on my old, 2017 ipad (first tab is this PR, second is
current prod, third is staging).
https://github.com/tldraw/tldraw/assets/2523721/327a7313-9273-4350-89a0-617a30fc01a2
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Regular culling shapes tests. Pan / zoom around. Use minimap. Change
pages.
- [x] Unit Tests
- [ ] End to end tests
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-10 10:29:11 +00:00
|
|
|
import { TestEditor } from './TestEditor'
|
|
|
|
import { TL } from './test-jsx'
|
|
|
|
|
|
|
|
let editor: TestEditor
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
editor = new TestEditor()
|
|
|
|
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
|
|
|
editor.renderingBoundsMargin = 100
|
|
|
|
})
|
|
|
|
|
|
|
|
function createShapes() {
|
|
|
|
return editor.createShapesFromJsx([
|
|
|
|
<TL.geo ref="A" x={100} y={100} w={100} h={100} />,
|
|
|
|
<TL.frame ref="B" x={200} y={200} w={300} h={300}>
|
|
|
|
<TL.geo ref="C" x={200} y={200} w={50} h={50} />
|
|
|
|
{/* this is outside of the frames clipping bounds, so it should never be rendered */}
|
|
|
|
<TL.geo ref="D" x={1000} y={1000} w={50} h={50} />
|
|
|
|
</TL.frame>,
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
it('lists shapes in viewport', () => {
|
|
|
|
const ids = createShapes()
|
|
|
|
editor.selectNone()
|
|
|
|
// D is clipped and so should always be culled / outside of viewport
|
|
|
|
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.D]))
|
|
|
|
|
|
|
|
// Move the camera 201 pixels to the right and 201 pixels down
|
|
|
|
editor.pan({ x: -201, y: -201 })
|
|
|
|
jest.advanceTimersByTime(500)
|
|
|
|
|
|
|
|
// A is now outside of the viewport
|
|
|
|
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.D]))
|
|
|
|
|
|
|
|
editor.pan({ x: -900, y: -900 })
|
|
|
|
jest.advanceTimersByTime(500)
|
|
|
|
// Now all shapes are outside of the viewport
|
|
|
|
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.B, ids.C, ids.D]))
|
|
|
|
|
|
|
|
editor.select(ids.B)
|
|
|
|
// We don't cull selected shapes
|
|
|
|
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.C, ids.D]))
|
|
|
|
|
|
|
|
editor.setEditingShape(ids.C)
|
|
|
|
// or shapes being edited
|
|
|
|
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.D]))
|
|
|
|
})
|
|
|
|
|
|
|
|
const shapeSize = 100
|
|
|
|
const numberOfShapes = 100
|
|
|
|
|
|
|
|
function getChangeOutsideBounds(viewportSize: number) {
|
|
|
|
const changeDirection = Math.random() > 0.5 ? 1 : -1
|
|
|
|
const maxChange = 1000
|
|
|
|
const changeAmount = 1 + Math.random() * maxChange
|
|
|
|
if (changeDirection === 1) {
|
|
|
|
// We need to get past the viewport size and then add a bit more
|
|
|
|
return viewportSize + changeAmount
|
|
|
|
} else {
|
|
|
|
// We also need to take the shape size into account
|
|
|
|
return -changeAmount - shapeSize
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getChangeInsideBounds(viewportSize: number) {
|
|
|
|
// We can go from -shapeSize to viewportSize
|
|
|
|
return -shapeSize + Math.random() * (viewportSize + shapeSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
function createFuzzShape(viewport: Box) {
|
|
|
|
const id = createShapeId()
|
|
|
|
if (Math.random() > 0.5) {
|
|
|
|
const positionChange = Math.random()
|
|
|
|
// Should x, or y, or both go outside the bounds?
|
|
|
|
const dimensionChange = positionChange < 0.33 ? 'x' : positionChange < 0.66 ? 'y' : 'both'
|
|
|
|
const xOutsideBounds = dimensionChange === 'x' || dimensionChange === 'both'
|
|
|
|
const yOutsideBounds = dimensionChange === 'y' || dimensionChange === 'both'
|
|
|
|
|
|
|
|
// Create a shape outside the viewport
|
|
|
|
editor.createShape({
|
|
|
|
id,
|
|
|
|
type: 'geo',
|
|
|
|
x:
|
|
|
|
viewport.x +
|
|
|
|
(xOutsideBounds ? getChangeOutsideBounds(viewport.w) : getChangeInsideBounds(viewport.w)),
|
|
|
|
y:
|
|
|
|
viewport.y +
|
|
|
|
(yOutsideBounds ? getChangeOutsideBounds(viewport.h) : getChangeInsideBounds(viewport.h)),
|
|
|
|
props: { w: shapeSize, h: shapeSize },
|
|
|
|
})
|
|
|
|
return { isCulled: true, id }
|
|
|
|
} else {
|
|
|
|
// Create a shape inside the viewport
|
|
|
|
editor.createShape({
|
|
|
|
id,
|
|
|
|
type: 'geo',
|
|
|
|
x: viewport.x + getChangeInsideBounds(viewport.w),
|
|
|
|
y: viewport.y + getChangeInsideBounds(viewport.h),
|
|
|
|
props: { w: shapeSize, h: shapeSize },
|
|
|
|
})
|
|
|
|
return { isCulled: false, id }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
it('correctly calculates the culled shapes when adding and deleting shapes', () => {
|
|
|
|
const viewport = editor.getViewportPageBounds()
|
|
|
|
const shapes: Array<TLShapeId | undefined> = []
|
|
|
|
for (let i = 0; i < numberOfShapes; i++) {
|
|
|
|
const { isCulled, id } = createFuzzShape(viewport)
|
|
|
|
shapes.push(id)
|
|
|
|
if (isCulled) {
|
|
|
|
expect(editor.getCulledShapes()).toContain(id)
|
|
|
|
} else {
|
|
|
|
expect(editor.getCulledShapes()).not.toContain(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const numberOfShapesToDelete = Math.floor((Math.random() * numberOfShapes) / 2)
|
|
|
|
for (let i = 0; i < numberOfShapesToDelete; i++) {
|
|
|
|
const index = Math.floor(Math.random() * (shapes.length - 1))
|
|
|
|
const id = shapes[index]
|
|
|
|
if (id) {
|
|
|
|
editor.deleteShape(id)
|
|
|
|
shapes[index] = undefined
|
|
|
|
expect(editor.getCulledShapes()).not.toContain(id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const culledShapesIncremental = editor.getCulledShapes()
|
|
|
|
|
|
|
|
// force full refresh
|
2024-04-16 10:56:35 +00:00
|
|
|
editor.pan({ x: -1, y: 0 })
|
|
|
|
editor.pan({ x: 1, y: 0 })
|
Perf: Incremental culled shapes calculation. (#3411)
Reworks our culling logic:
- No longer show the gray rectangles for culled shapes.
- Don't use `renderingBoundExpanded`, instead we now use
`viewportPageBounds`. I've removed `renderingBoundsExpanded`, but we
might want to deprecate it?
- There's now a incremental computation of non visible shapes, which are
shapes outside of `viewportPageBounds` and shapes that outside of their
parents' clipping bounds.
- There's also a new `getCulledShapes` function in `Editor`, which uses
the non visible shapes computation as a part of the culled shape
computation.
- Also moved some of the `getRenderingShapes` tests to newly created
`getCullingShapes` tests.
Feels much better on my old, 2017 ipad (first tab is this PR, second is
current prod, third is staging).
https://github.com/tldraw/tldraw/assets/2523721/327a7313-9273-4350-89a0-617a30fc01a2
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Regular culling shapes tests. Pan / zoom around. Use minimap. Change
pages.
- [x] Unit Tests
- [ ] End to end tests
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-10 10:29:11 +00:00
|
|
|
|
|
|
|
const culledShapeFromScratch = editor.getCulledShapes()
|
|
|
|
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
|
|
|
|
})
|