Add SpatialIndexManager for R-tree spatial indexing#7676
Add SpatialIndexManager for R-tree spatial indexing#7676MitjaBezensek merged 31 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
|
API Changes Check Passed Great! The PR description now includes the required "### API changes" section. This helps reviewers and SDK users understand the impact of your changes. |
092d552 to
10276a3
Compare
|
Some quick performance tests using the added logging. This is while having many shapes on the page. With a small number of shapes the performance is pretty close if not slightly worse. Erasing📊 [Perf Summary] erasing (old) 📊 [Perf Summary] erasing (spatial) Panning (tests culling)📊 [Perf Summary] notVisibleShapes (old) 📊 [Perf Summary] notVisibleShapes (spatial) Zooming📊 [Perf Summary] notVisibleShapes (old) 📊 [Perf Summary] notVisibleShapes (spatial) Switching pages (has to build the index as it is per page)[perf] setCurrentPage (old) (total with render+paint): 755.30ms [perf] setCurrentPage (spatial) (total with render+paint): 786.00ms Get shape at point📊 [Perf Summary] getShapeAtPoint (old) 📊 [Perf Summary] getShapeAtPoint (spatial) |
9bf9222 to
00fd34d
Compare
0a7e4c0 to
418e9e5
Compare
…ounds first before doing an early return. Optiize early return if no shapes have changed.
…moving shapes between pages.
|
(sorry about previous comments, rouge agent) |
ds300
left a comment
There was a problem hiding this comment.
This looks excellent! The incremental derivation seems to be updated correctly and the perf gains are fantastic. Well done mitja!
2b977ea to
c7daa84
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| } | ||
|
|
||
| const allShapes = editor.getCurrentPageRenderingShapesSorted() | ||
| const currentPageShapes = allShapes.filter((shape) => candidateIds.has(shape.id)) |
There was a problem hiding this comment.
Frames not checked when spatial index returns no candidates
Medium Severity
Frame labels are positioned above frame shapes, outside their bounds. In Editor.ts, getShapeAtPoint and getShapesAtPoint handle this by always including frames regardless of spatial index results. However, the erasing, brushing, and scribble brushing tools filter shapes using only candidateIds.has(shape.id) without the frame exception. This causes frames to be missed when only their label intersects the erase/brush area but their body bounds don't.
Additional Locations (2)
| processedShapeIds.add(to.id) | ||
|
|
||
| const wasOnPage = this.editor.getAncestorPageId(from) === this.lastPageId | ||
| const isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId |
There was a problem hiding this comment.
Page change detection uses current state, not historical state
Medium Severity
The getAncestorPageId method always fetches the current shape state via this.getShape(id), ignoring the passed shape object. When checking wasOnPage using from (the old state), it actually returns the current page, not the previous page. This causes shapes moved away from the current page to remain in the spatial index incorrectly, since both wasOnPage and isOnPage will return the same value (current state), and the removal branch is never taken.
Follow-up to #7676. I've reconsidered and made the spatial index private. This PR makes the SpatialIndexManager internal to the editor rather than part of the public API. The spatial index is an implementation detail used for efficient shape queries. Changes: - Mark `SpatialIndexManager` class as `@internal` - Remove `editor.spatialIndex` public property (now `private _spatialIndex`) - Mark `editor.getShapeIdsInsideBounds()` as `@internal` The public API for shape queries remains `editor.getShapeAtPoint()` and `editor.getShapesAtPoint()`, which handle edge cases like frame labels being outside their bounds. ### Change type - [x] `api` ### API changes - Removed `editor.spatialIndex` property from public API - Changed `editor.getShapeIdsInsideBounds()` from `@public` to `@internal` - Changed `SpatialIndexManager` class from `@public` to `@internal` ### Test plan 1. Existing tests pass 2. Internal usages in brushing/erasing tools still work ### Release notes - Remove `editor.spatialIndex` from public API <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Makes spatial indexing an internal implementation detail and removes it from the public API. > > - Mark `SpatialIndexManager` as `@internal`; update class docs accordingly > - Remove public `editor.spatialIndex`; introduce private `editor._spatialIndex` and update usages in hit-testing and bounds queries > - Mark `editor.getShapeIdsInsideBounds()` as `@internal` and clarify its bounds-only behavior > - Public querying remains via `editor.getShapeAtPoint()` / `editor.getShapesAtPoint()` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f366bfb. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Adds SpatialIndexManager using an R-tree (RBush) for O(log n) spatial queries instead of O(n) iteration.
pr-7676-walkthrough.mp4
Changes
SpatialIndexManager - R-tree based spatial index
editor.spatialIndexEarly return optimizations for tools
Upgraded RBush to v4.0.1
API changes
New exports from @tldraw/editor
New editor property
Methods:
getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId>— Returns shape IDs that intersect with the given rectangular area.getShapeIdsAtPoint(point: {x, y}, margin?: number): Set<TLShapeId>— Returns shape IDs near a point within an optional margin.dispose(): void— Cleans up the spatial index.Change type
improvementTest plan
Release notes
SpatialIndexManagerfor efficient spatial queries using R-tree indexingeditor.spatialIndexAPI withgetShapeIdsInsideBounds()andgetShapeIdsAtPoint()methodsNote
Adds an R-tree–backed spatial index and integrates it across core editor paths for faster queries.
SpatialIndexManager(exported) witheditor.spatialIndexprovidinggetShapeIdsInsideBoundsandgetShapeIdsAtPointgetShapesAtPointand addsgetShapeIdsInsideBounds(bounds); refinesgetShapeAtPointcandidate filtering and increases search margin; maintains per-page index with incremental updatesnotVisibleShapesto use spatial index; adds cached equality ingetCulledShapesfor stable Set identity; minor canvas reflow optimization via reference checkBrushing,ScribbleBrushing,Erasing) use spatial candidates and early returns to avoid full-shape iterationrbushand@types/rbush; export insrc/index.ts; add comprehensive tests for visibility/cullingWritten by Cursor Bugbot for commit 7686e2f. This will update automatically on new commits. Configure here.