Revert "RBush again? (#3439)" (#3481)

This reverts commit 45dffd1af6.

Revert rbush. There's issues with shapes that have computed bounds
(arrows, groups).

### 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


- Add a brief release note for your PR here.
pull/3487/head
Mitja Bezenšek 2024-04-16 12:56:35 +02:00 zatwierdzone przez GitHub
rodzic cb118ef712
commit 88ee4e9993
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
14 zmienionych plików z 132 dodań i 570 usunięć

Wyświetl plik

@ -188,8 +188,6 @@ export interface BoundsSnapPoint {
export class Box {
constructor(x?: number, y?: number, w?: number, h?: number);
// (undocumented)
static AroundPoint(point: VecLike, n: number): Box;
// (undocumented)
get aspectRatio(): number;
// (undocumented)
get center(): Vec;
@ -757,7 +755,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getShapeClipPath(shape: TLShape | TLShapeId): string | undefined;
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T;
getShapeHandles<T extends TLShape>(shape: T | T['id']): TLHandle[] | undefined;
getShapeIdsInsideBounds(bounds: Box): TLShapeId[];
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat;
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined;
@ -769,7 +766,6 @@ export class Editor extends EventEmitter<TLEventMap> {
hitInside?: boolean | undefined;
margin?: number | undefined;
}): TLShape[];
getShapesInsideBounds(bounds: Box): TLShape[];
// (undocumented)
getShapeStyleIfExists<T>(shape: TLShape, style: StyleProp<T>): T | undefined;
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>;

Wyświetl plik

@ -1623,72 +1623,6 @@
}
]
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Box.AroundPoint:member(1)",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "static AroundPoint(point: "
},
{
"kind": "Reference",
"text": "VecLike",
"canonicalReference": "@tldraw/editor!VecLike:type"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": true,
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "point",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "AroundPoint"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!Box#aspectRatio:member",
@ -12800,60 +12734,6 @@
"isAbstract": false,
"name": "getShapeHandles"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getShapeIdsInsideBounds:member(1)",
"docComment": "/**\n * Get the shapes ids of shapes that are are (at least partially) inside the bounds.\n *\n * @param bounds - The bounds to check.\n *\n * @returns The shape ids of shapes that are at least partially inside the bounds.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getShapeIdsInsideBounds(bounds: "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "bounds",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "getShapeIdsInsideBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getShapeLocalTransform:member(1)",
@ -13357,60 +13237,6 @@
"isAbstract": false,
"name": "getShapesAtPoint"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getShapesInsideBounds:member(1)",
"docComment": "/**\n * Get the shapes that are are (at least partially) inside the bounds.\n *\n * @param bounds - The bounds to check.\n *\n * @returns The shapes that are at least partially inside the bounds.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getShapesInsideBounds(bounds: "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "bounds",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "getShapesInsideBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getShapeStyleIfExists:member(1)",

Wyświetl plik

@ -59,8 +59,7 @@
"is-plain-object": "^5.0.0",
"lodash.throttle": "^4.1.1",
"lodash.uniq": "^4.5.0",
"nanoid": "4.0.2",
"rbush": "^3.0.1"
"nanoid": "4.0.2"
},
"peerDependencies": {
"react": "^18",
@ -73,7 +72,6 @@
"@types/benchmark": "^2.1.2",
"@types/lodash.throttle": "^4.1.7",
"@types/lodash.uniq": "^4.5.7",
"@types/rbush": "^3.0.3",
"@types/react-test-renderer": "^18.0.0",
"@types/wicg-file-system-access": "^2020.9.5",
"benchmark": "^2.1.4",

Wyświetl plik

@ -100,9 +100,9 @@ import { getReorderingShapesChanges } from '../utils/reorderShapes'
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
import { uniqueId } from '../utils/uniqueId'
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
import { notVisibleShapes } from './derivations/notVisibleShapes'
import { parentsToChildren } from './derivations/parentsToChildren'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
import { SpatialIndex } from './derivations/spatialIndex'
import { getSvgJsx } from './getSvgJsx'
import { ClickManager } from './managers/ClickManager'
import { EnvironmentManager } from './managers/EnvironmentManager'
@ -589,7 +589,6 @@ export class Editor extends EventEmitter<TLEventMap> {
)
this._parentIdsToChildIds = parentsToChildren(this.store)
this._spatialIndex = new SpatialIndex(this)
this.disposables.add(
this.store.listen((changes) => {
this.emit('change', changes)
@ -4199,8 +4198,10 @@ export class Editor extends EventEmitter<TLEventMap> {
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
}
/* @internal */
private readonly _spatialIndex: SpatialIndex
@computed
private _notVisibleShapes() {
return notVisibleShapes(this)
}
/**
* Get culled shapes.
@ -4209,7 +4210,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
@computed
getCulledShapes() {
const notVisibleShapes = this._spatialIndex.getNotVisibleShapes()
const notVisibleShapes = this._notVisibleShapes().get()
const selectedShapeIds = this.getSelectedShapeIds()
const editingId = this.getEditingShapeId()
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
@ -4224,31 +4225,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return culledShapes
}
/**
* Get the shapes ids of shapes that are are (at least partially) inside the bounds.
*
* @param bounds - The bounds to check.
* @returns The shape ids of shapes that are at least partially inside the bounds.
*
* @public
*/
getShapeIdsInsideBounds(bounds: Box): TLShapeId[] {
return this._spatialIndex.getShapeIdsInsideBounds(bounds)
}
/**
* Get the shapes that are are (at least partially) inside the bounds.
*
* @param bounds - The bounds to check.
* @returns The shapes that are at least partially inside the bounds.
*
* @public
*/
getShapesInsideBounds(bounds: Box): TLShape[] {
const shapeIds = this.getShapeIdsInsideBounds(bounds)
return compact(shapeIds.map((id) => this.getShape(id)))
}
/**
* The bounds of the current page (the common bounds of all of the shapes on the page).
*
@ -4278,18 +4254,9 @@ export class Editor extends EventEmitter<TLEventMap> {
* @returns The top-most selected shape at the given point, or undefined if there is no shape at the point.
*/
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined {
const shapesCloseToPoint = new Set(
this.getShapeIdsInsideBounds(Box.AroundPoint(point, HIT_TEST_MARGIN))
)
const selectedShapeIds = this.getSelectedShapeIds()
return this.getCurrentPageShapesSorted()
.filter(
(shape) =>
shape.type !== 'group' &&
shapesCloseToPoint.has(shape.id) &&
selectedShapeIds.includes(shape.id)
)
.filter((shape) => shape.type !== 'group' && selectedShapeIds.includes(shape.id))
.reverse() // findlast
.find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }))
}
@ -4329,15 +4296,11 @@ export class Editor extends EventEmitter<TLEventMap> {
let inMarginClosestToEdgeDistance = Infinity
let inMarginClosestToEdgeHit: TLShape | null = null
const shapesCloseToPoint = new Set(
this.getShapeIdsInsideBounds(Box.AroundPoint(point, HIT_TEST_MARGIN))
)
const shapesToCheck = (
opts.renderingOnly
? this.getCurrentPageRenderingShapesSorted()
: this.getCurrentPageShapesSorted()
).filter((shape) => {
if (!shapesCloseToPoint.has(shape.id)) return
if (this.isShapeOfType(shape, 'group')) return false
const pageMask = this.getShapeMask(shape)
if (pageMask && !pointInPolygon(point, pageMask)) return false

Wyświetl plik

@ -0,0 +1,105 @@
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
import { Box } from '../../primitives/Box'
import { Editor } from '../Editor'
function isShapeNotVisible(editor: Editor, id: TLShapeId, viewportPageBounds: Box): boolean {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
// if the shape is fully outside of its parent's clipping bounds...
if (maskedPageBounds === undefined) return true
// if the shape is fully outside of the viewport page bounds...
return !viewportPageBounds.includes(maskedPageBounds)
}
/**
* Incremental derivation of not visible shapes.
* Non visible shapes are shapes outside of the viewport page bounds and shapes outside of parent's clipping bounds.
*
* @param editor - Instance of the tldraw Editor.
* @returns Incremental derivation of non visible shapes.
*/
export const notVisibleShapes = (editor: Editor) => {
const isCullingOffScreenShapes = Number.isFinite(editor.renderingBoundsMargin)
const shapeHistory = editor.store.query.filterHistory('shape')
let lastPageId: TLPageId | null = null
let prevViewportPageBounds: Box
function fromScratch(editor: Editor): Set<TLShapeId> {
const shapes = editor.getCurrentPageShapeIds()
lastPageId = editor.getCurrentPageId()
const viewportPageBounds = editor.getViewportPageBounds()
prevViewportPageBounds = viewportPageBounds.clone()
const notVisibleShapes = new Set<TLShapeId>()
shapes.forEach((id) => {
if (isShapeNotVisible(editor, id, viewportPageBounds)) {
notVisibleShapes.add(id)
}
})
return notVisibleShapes
}
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue, lastComputedEpoch) => {
if (!isCullingOffScreenShapes) return new Set<TLShapeId>()
if (isUninitialized(prevValue)) {
return fromScratch(editor)
}
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
if (diff === RESET_VALUE) {
return fromScratch(editor)
}
const currentPageId = editor.getCurrentPageId()
if (lastPageId !== currentPageId) {
return fromScratch(editor)
}
const viewportPageBounds = editor.getViewportPageBounds()
if (!prevViewportPageBounds || !viewportPageBounds.equals(prevViewportPageBounds)) {
return fromScratch(editor)
}
let nextValue = null as null | Set<TLShapeId>
const addId = (id: TLShapeId) => {
// Already added
if (prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.add(id)
}
const deleteId = (id: TLShapeId) => {
// No need to delete since it's not there
if (!prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.delete(id)
}
for (const changes of diff) {
for (const record of Object.values(changes.added)) {
if (isShape(record)) {
const isCulled = isShapeNotVisible(editor, record.id, viewportPageBounds)
if (isCulled) {
addId(record.id)
}
}
}
for (const [_from, to] of Object.values(changes.updated)) {
if (isShape(to)) {
const isCulled = isShapeNotVisible(editor, to.id, viewportPageBounds)
if (isCulled) {
addId(to.id)
} else {
deleteId(to.id)
}
}
}
for (const id of Object.keys(changes.removed)) {
if (isShapeId(id)) {
deleteId(id)
}
}
}
return nextValue ?? prevValue
})
}

Wyświetl plik

@ -1,172 +0,0 @@
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
import RBush from 'rbush'
import { Box } from '../../primitives/Box'
import { Editor } from '../Editor'
type Element = {
minX: number
minY: number
maxX: number
maxY: number
id: TLShapeId
}
class TldrawRBush extends RBush<Element> {}
export class SpatialIndex {
private readonly spatialIndex: ReturnType<typeof this.createSpatialIndex>
private lastPageId: TLPageId | null = null
private shapesInTree: Map<TLShapeId, Element>
private rBush: TldrawRBush
constructor(private editor: Editor) {
this.spatialIndex = this.createSpatialIndex()
this.shapesInTree = new Map<TLShapeId, Element>()
this.rBush = new TldrawRBush()
}
private addElement(id: TLShapeId, a: Element[], existingBounds?: Box) {
const e = this.getElement(id, existingBounds)
if (!e) return
a.push(e)
this.shapesInTree.set(id, e)
}
private getElement(id: TLShapeId, existingBounds?: Box): Element | null {
const bounds = existingBounds ?? this.editor.getShapeMaskedPageBounds(id)
if (!bounds) return null
return {
minX: bounds.minX,
minY: bounds.minY,
maxX: bounds.maxX,
maxY: bounds.maxY,
id,
}
}
private fromScratch(lastComputedEpoch: number) {
this.lastPageId = this.editor.getCurrentPageId()
this.shapesInTree = new Map<TLShapeId, Element>()
const elementsToAdd: Element[] = []
this.editor.getCurrentPageShapeIds().forEach((id) => {
this.addElement(id, elementsToAdd)
})
this.rBush = new TldrawRBush().load(elementsToAdd)
return lastComputedEpoch
}
private createSpatialIndex() {
const shapeHistory = this.editor.store.query.filterHistory('shape')
return computed<number>('spatialIndex', (prevValue, lastComputedEpoch) => {
if (isUninitialized(prevValue)) {
return this.fromScratch(lastComputedEpoch)
}
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
if (diff === RESET_VALUE) {
return this.fromScratch(lastComputedEpoch)
}
const currentPageId = this.editor.getCurrentPageId()
if (!this.lastPageId || this.lastPageId !== currentPageId) {
return this.fromScratch(lastComputedEpoch)
}
let isDirty = false
for (const changes of diff) {
const elementsToAdd: Element[] = []
for (const record of Object.values(changes.added)) {
if (isShape(record)) {
this.addElement(record.id, elementsToAdd)
}
}
for (const [_from, to] of Object.values(changes.updated)) {
if (isShape(to)) {
const currentElement = this.shapesInTree.get(to.id)
const newBounds = this.editor.getShapeMaskedPageBounds(to.id)
if (currentElement) {
if (
newBounds?.minX === currentElement.minX &&
newBounds.minY === currentElement.minY &&
newBounds.maxX === currentElement.maxX &&
newBounds.maxY === currentElement.maxY
) {
continue
}
this.shapesInTree.delete(to.id)
this.rBush.remove(currentElement)
isDirty = true
}
this.addElement(to.id, elementsToAdd, newBounds)
}
}
if (elementsToAdd.length) {
this.rBush.load(elementsToAdd)
isDirty = true
}
for (const id of Object.keys(changes.removed)) {
if (isShapeId(id)) {
const currentElement = this.shapesInTree.get(id)
if (currentElement) {
this.shapesInTree.delete(id)
this.rBush.remove(currentElement)
isDirty = true
}
}
}
}
return isDirty ? lastComputedEpoch : prevValue
})
}
@computed
private _getVisibleShapes() {
return computed<Set<TLShapeId>>('visible shapes', (prevValue) => {
// Make sure the spatial index is up to date
const _index = this.spatialIndex.get()
const newValue = this.rBush.search(this.editor.getViewportPageBounds()).map((s) => s.id)
if (isUninitialized(prevValue)) {
return new Set(newValue)
}
const isSame = prevValue.size === newValue.length && newValue.every((id) => prevValue.has(id))
return isSame ? prevValue : new Set(newValue)
})
}
@computed
getVisibleShapes() {
return this._getVisibleShapes().get()
}
@computed
_getNotVisibleShapes() {
return computed<Set<TLShapeId>>('not visible shapes', (prevValue) => {
const visibleShapes = this._getVisibleShapes().get()
const pageShapes = this.editor.getCurrentPageShapeIds()
const nonVisibleShapes = [...pageShapes].filter((id) => !visibleShapes.has(id))
if (isUninitialized(prevValue)) return new Set(nonVisibleShapes)
const isSame =
prevValue.size === nonVisibleShapes.length &&
nonVisibleShapes.every((id) => prevValue.has(id))
return isSame ? prevValue : new Set(nonVisibleShapes)
})
}
@computed
getNotVisibleShapes() {
return this._getNotVisibleShapes().get()
}
getShapeIdsInsideBounds(bounds: Box) {
// Make sure the spatial index is up to date
const _index = this.spatialIndex.get()
return this.rBush.search(bounds).map((s) => s.id)
}
}

Wyświetl plik

@ -376,10 +376,6 @@ export class Box {
return new Box(minX, minY, maxX - minX, maxY - minY)
}
static AroundPoint(point: VecLike, n: number) {
return new Box(point.x - n, point.y - n, 2 * n, 2 * n)
}
static Expand(A: Box, B: Box) {
const minX = Math.min(B.minX, A.minX)
const minY = Math.min(B.minY, A.minY)

Wyświetl plik

@ -1,5 +1,4 @@
import {
Box,
HIT_TEST_MARGIN,
StateNode,
TLEventHandlers,
@ -81,8 +80,9 @@ export class Erasing extends StateNode {
update() {
const { editor, excludedShapeIds } = this
const erasingShapeIds = this.editor.getErasingShapeIds()
const zoomLevel = this.editor.getZoomLevel()
const erasingShapeIds = editor.getErasingShapeIds()
const zoomLevel = editor.getZoomLevel()
const currentPageShapes = editor.getCurrentPageShapes()
const {
inputs: { currentPagePoint, previousPagePoint },
} = editor
@ -92,11 +92,8 @@ export class Erasing extends StateNode {
const erasing = new Set<TLShapeId>(erasingShapeIds)
const minDist = HIT_TEST_MARGIN / zoomLevel
const shapesNearPoint = this.editor.getShapesInsideBounds(
Box.FromPoints([currentPagePoint, previousPagePoint]).expandBy(minDist)
)
for (const shape of shapesNearPoint) {
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
for (const shape of currentPageShapes) {
if (editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
// Avoid testing masked shapes, unless the pointer is inside the mask
const pageMask = editor.getShapeMask(shape.id)

Wyświetl plik

@ -125,13 +125,12 @@ export class Brushing extends StateNode {
pageTransform: Mat | undefined,
localCorners: Vec[]
const currentPageShapes = editor.getCurrentPageShapes()
const currentPageId = editor.getCurrentPageId()
const shapesInsideBounds = this.editor.getShapesInsideBounds(brush)
testAllShapes: for (let i = 0, n = shapesInsideBounds.length; i < n; i++) {
shape = shapesInsideBounds[i]
if (excludedShapeIds.has(shape.id)) continue testAllShapes
if (results.has(shape.id)) continue testAllShapes
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
shape = currentPageShapes[i]
if (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes
pageBounds = editor.getShapePageBounds(shape)
if (!pageBounds) continue testAllShapes
@ -177,7 +176,7 @@ export class Brushing extends StateNode {
const current = editor.getSelectedShapeIds()
if (current.length !== results.size || current.some((id) => !results.has(id))) {
editor.setSelectedShapes(Array.from(results).sort(), { squashing: true })
editor.setSelectedShapes(Array.from(results), { squashing: true })
}
}

Wyświetl plik

@ -1,5 +1,4 @@
import {
Box,
Geometry2d,
StateNode,
TLEventHandlers,
@ -84,6 +83,8 @@ export class ScribbleBrushing extends StateNode {
private updateScribbleSelection(addPoint: boolean) {
const { editor } = this
// const zoomLevel = this.editor.getZoomLevel()
const currentPageShapes = this.editor.getCurrentPageShapes()
const {
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
} = this.editor
@ -94,9 +95,7 @@ export class ScribbleBrushing extends StateNode {
this.pushPointToScribble()
}
const shapes = this.editor.getShapesInsideBounds(
Box.FromPoints([previousPagePoint, currentPagePoint])
)
const shapes = currentPageShapes
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
const minDist = 0 // HIT_TEST_MARGIN / zoomLevel

Wyświetl plik

@ -1,4 +1,4 @@
import { Box, PageRecordType, TLShapeId, createShapeId } from '@tldraw/editor'
import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
import { TL } from './test-jsx'
@ -130,14 +130,8 @@ it('correctly calculates the culled shapes when adding and deleting shapes', ()
const culledShapesIncremental = editor.getCulledShapes()
// force full refresh
const currentPage = editor.getCurrentPageId()
const id = PageRecordType.createId('page2')
editor.createPage({ id, name: 'Page 2' })
editor.setCurrentPage(id)
// Make sure we access the culled shapes so that the editor knows to update them
// In prod this happens automatically since rendering of shapes depends on the culled shapes
const _culledOnOtherPage = editor.getCulledShapes()
editor.setCurrentPage(currentPage)
editor.pan({ x: -1, y: 0 })
editor.pan({ x: 1, y: 0 })
const culledShapeFromScratch = editor.getCulledShapes()
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)

Wyświetl plik

@ -1612,16 +1612,16 @@ describe('shift brushes to add to the selection', () => {
editor.keyDown('Shift')
editor.pointerDown()
editor.pointerMove(1, 1)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
editor.keyUp('Shift')
// there's a timer here—we should keep the shift mode until the timer expires
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
jest.advanceTimersByTime(500)
// once the timer expires, we should be back in regular mode
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.keyDown('Shift')
// there's no timer on key down, so go right into shift mode again
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
})
})

Wyświetl plik

@ -1,114 +0,0 @@
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)
})
})

Wyświetl plik

@ -7489,7 +7489,6 @@ __metadata:
"@types/core-js": "npm:^2.5.5"
"@types/lodash.throttle": "npm:^4.1.7"
"@types/lodash.uniq": "npm:^4.5.7"
"@types/rbush": "npm:^3.0.3"
"@types/react-test-renderer": "npm:^18.0.0"
"@types/wicg-file-system-access": "npm:^2020.9.5"
"@use-gesture/react": "npm:^10.2.27"
@ -7506,7 +7505,6 @@ __metadata:
lodash.throttle: "npm:^4.1.1"
lodash.uniq: "npm:^4.5.0"
nanoid: "npm:4.0.2"
rbush: "npm:^3.0.1"
react-test-renderer: "npm:^18.2.0"
resize-observer-polyfill: "npm:^1.5.1"
peerDependencies:
@ -8333,13 +8331,6 @@ __metadata:
languageName: node
linkType: hard
"@types/rbush@npm:^3.0.3":
version: 3.0.3
resolution: "@types/rbush@npm:3.0.3"
checksum: 59c75d20d3ebf95f8853a98f67d437adc047bf875df6e6bba90884fdfa8fa927402ccec762ecbc8724d98f9ed14c9e97d16eddb709a702021ce1874da5d0d8d7
languageName: node
linkType: hard
"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.18":
version: 18.2.18
resolution: "@types/react-dom@npm:18.2.18"
@ -21079,13 +21070,6 @@ __metadata:
languageName: node
linkType: hard
"quickselect@npm:^2.0.0":
version: 2.0.0
resolution: "quickselect@npm:2.0.0"
checksum: ed2e78431050d223fb75da20ee98011aef1a03f7cb04e1a32ee893402e640be3cfb76d72e9dbe01edf3bb457ff6a62e5c2d85748424d1aa531f6ba50daef098c
languageName: node
linkType: hard
"raf@npm:^3.4.1":
version: 3.4.1
resolution: "raf@npm:3.4.1"
@ -21123,15 +21107,6 @@ __metadata:
languageName: node
linkType: hard
"rbush@npm:^3.0.1":
version: 3.0.1
resolution: "rbush@npm:3.0.1"
dependencies:
quickselect: "npm:^2.0.0"
checksum: 489e2e7d9889888ad533518f194e3ab7cc19b1f1365a38ee99fbdda542a47f41cda7dc89870180050f4d04ea402e9ff294e1d767d03c0f1694e0028b7609eec9
languageName: node
linkType: hard
"rc@npm:^1.2.7, rc@npm:^1.2.8, rc@npm:~1.2.7":
version: 1.2.8
resolution: "rc@npm:1.2.8"