Tldraw/packages/tldraw/src/test/spatialIndex.test.ts

115 wiersze
4.0 KiB
TypeScript
Czysty Zwykły widok Historia

RBush again? (#3439) Adds RBush to handle spatial querying. We use it for: - Culling. Helps a lot with panning as we don't have to compute the culled shapes from scratch. Instead we just query rbush again. It makes culling quite granular: spatial index updates when shapes change (additions, removals, changes to bounds), visible shapes depends on that, but also updates when the viewport page bound change, culled shapes then depend on that but also change with selections changes. The api stayed the same, which is great since the fuzz tests can stay as they are. - Brushing - Erasing - Scribble brushing - Getting shapes at point (for example, when updating the hover id) This improves performance of all of those operations. I might have missed some places where this might also be useful. ### Erasing before (Test on my old ipad) https://github.com/tldraw/tldraw/assets/2523721/edb9c004-a44a-4779-b2d0-98617b057314 ### Erasing after https://github.com/tldraw/tldraw/assets/2523721/8f8367fd-fa8e-4963-ba13-720c5f0c2da5 ### Creating an arrow before https://github.com/tldraw/tldraw/assets/2523721/4068f8b7-f7b8-4826-83f2-083b1f3783bc ### After (much better, but still bad) https://github.com/tldraw/tldraw/assets/2523721/11af6be6-01d8-4740-bf15-896e2dd31dd6 ### 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 ❗️ --> - [ ] `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 --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-15 16:28:18 +00:00
import { Box, PageRecordType, TLShapeId, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
})
const NUM_SHAPES = 1000
const SHAPE_SIZE = { min: 100, max: 300 }
const NUM_QUERIES = 100
type IdAndBounds = { id: TLShapeId; bounds: Box }
function generateShapes() {
const result: IdAndBounds[] = []
for (let i = 0; i < NUM_SHAPES; i++) {
const xNegative = Math.random() > 0.5
const yNegative = Math.random() > 0.5
const x = Math.random() * 10000 * (xNegative ? -1 : 1)
const y = Math.random() * 10000 * (yNegative ? -1 : 1)
const id = createShapeId()
editor.createShape({
id,
type: 'geo',
x,
y,
props: {
w: Math.random() * (SHAPE_SIZE.max - SHAPE_SIZE.min) + SHAPE_SIZE.min,
h: Math.random() * (SHAPE_SIZE.max - SHAPE_SIZE.min) + SHAPE_SIZE.min,
},
})
const shape = editor.getShape(id)
if (!shape) continue
const bounds = editor.getShapePageBounds(shape)
if (!bounds) continue
result.push({ id, bounds })
}
return result
}
function pickShapes(shapes: IdAndBounds[]) {
// We pick at max 1/40 of the shapes, so that the common bounds have more chance not to cover the whole area
const numOfShapes = Math.floor((Math.random() * NUM_SHAPES) / 40)
const pickedShapes: IdAndBounds[] = []
for (let i = 0; i < numOfShapes; i++) {
const index = Math.floor(Math.random() * shapes.length)
pickedShapes.push(shapes[index])
}
return pickedShapes
}
describe('Spatial Index', () => {
it('finds the shapes inside and outside bounds', () => {
const shapes = generateShapes()
for (let i = 0; i < NUM_QUERIES; i++) {
const pickedShapes = pickShapes(shapes)
const commonBounds = Box.Common(pickedShapes.map((s) => s.bounds))
let shapeIdsInsideBounds = editor.getShapeIdsInsideBounds(commonBounds)
// It should include all the shapes inside common bounds
expect(pickedShapes.every((s) => shapeIdsInsideBounds.includes(s.id))).toBe(true)
// It also works when we shrink the bounds so that we don't fully contain shapes
shapeIdsInsideBounds = editor.getShapeIdsInsideBounds(
commonBounds.expandBy(-SHAPE_SIZE.min / 2)
)
expect(pickedShapes.every((s) => shapeIdsInsideBounds.includes(s.id))).toBe(true)
const shapeIdsOutsideBounds = shapes
.map((i) => i.id)
.filter((id) => {
const shape = editor.getShape(id)
if (!shape) return false
const bounds = editor.getShapePageBounds(shape)
if (!bounds) return false
return !commonBounds.includes(bounds)
})
// It should not contain any shapes outside the bounds
expect(shapeIdsOutsideBounds.every((id) => !shapeIdsInsideBounds.includes(id))).toBe(true)
expect(shapeIdsInsideBounds.length + shapeIdsOutsideBounds.length).toBe(NUM_SHAPES)
}
})
it('works when switching pages', () => {
const currentPageId = editor.getCurrentPageId()
let shapesInsideBounds: TLShapeId[]
const page1Shapes = generateShapes()
const page1Picks = pickShapes(page1Shapes)
const page1CommonBounds = Box.Common(page1Picks.map((s) => s.bounds))
shapesInsideBounds = editor.getShapeIdsInsideBounds(page1CommonBounds)
expect(page1Picks.every((s) => shapesInsideBounds.includes(s.id))).toBe(true)
const newPage = {
id: PageRecordType.createId(),
name: 'Page 2',
}
editor.createPage(newPage)
editor.setCurrentPage(newPage.id)
const page2Shapes = generateShapes()
const page2Picks = pickShapes(page2Shapes)
const page2CommonBounds = Box.Common(page2Picks.map((s) => s.bounds))
shapesInsideBounds = editor.getShapeIdsInsideBounds(page2CommonBounds)
expect(page2Picks.every((s) => shapesInsideBounds.includes(s.id))).toBe(true)
expect(page1Shapes.every((s) => !shapesInsideBounds.includes(s.id))).toBe(true)
editor.setCurrentPage(currentPageId)
shapesInsideBounds = editor.getShapeIdsInsideBounds(page1CommonBounds)
expect(page1Picks.every((s) => shapesInsideBounds.includes(s.id))).toBe(true)
expect(page2Shapes.every((s) => !shapesInsideBounds.includes(s.id))).toBe(true)
})
})