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)
|
|
|
|
})
|
Fix culling. (#3504)
Fixes culling for cases when another user would drag shapes inside your
viewport. We weren't correctly calculating the culling status for arrows
that might be bound to those shapes and also for shapes within dragged
in groups / frames.
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [ ] `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
- [x] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `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. Open the same room in two browsers / tabs.
2. Have some shapes that are visible in one browser, but not the other.
3. Drag these shapes so that they are visible in the other browser as
well.
4. They should correctly get unculled.
5. Do this by dragging shapes that have arrows bound to them (arrows
should uncull), groups (shapes within them should uncull), frames.
- [x] Unit Tests
- [ ] End to end tests
### Release Notes
- Fix culling.
2024-04-17 11:39:09 +00:00
|
|
|
|
|
|
|
it('works for shapes that are outside of the viewport, but are then moved inside it', () => {
|
|
|
|
const box1Id = createShapeId()
|
|
|
|
const box2Id = createShapeId()
|
|
|
|
const arrowId = createShapeId()
|
|
|
|
|
|
|
|
editor.createShapes([
|
|
|
|
{
|
|
|
|
id: box1Id,
|
|
|
|
props: { w: 100, h: 100, geo: 'rectangle' },
|
|
|
|
type: 'geo',
|
|
|
|
x: -500,
|
|
|
|
y: 0,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: box2Id,
|
|
|
|
type: 'geo',
|
|
|
|
x: -1000,
|
|
|
|
y: 200,
|
|
|
|
props: { w: 100, h: 100, geo: 'rectangle' },
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: arrowId,
|
|
|
|
type: 'arrow',
|
|
|
|
props: {
|
|
|
|
start: {
|
|
|
|
type: 'binding',
|
|
|
|
isExact: true,
|
|
|
|
boundShapeId: box1Id,
|
|
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
|
|
isPrecise: false,
|
|
|
|
},
|
|
|
|
end: {
|
|
|
|
type: 'binding',
|
|
|
|
isExact: true,
|
|
|
|
boundShapeId: box2Id,
|
|
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
|
|
isPrecise: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
])
|
|
|
|
|
|
|
|
expect(editor.getCulledShapes()).toEqual(new Set([box1Id, box2Id, arrowId]))
|
|
|
|
|
|
|
|
// Move box1 and box2 inside the viewport
|
|
|
|
editor.updateShapes([
|
|
|
|
{ id: box1Id, type: 'geo', x: 100 },
|
|
|
|
{ id: box2Id, type: 'geo', x: 200 },
|
|
|
|
])
|
|
|
|
// Arrow should also not be culled
|
|
|
|
expect(editor.getCulledShapes()).toEqual(new Set())
|
|
|
|
})
|