kopia lustrzana https://github.com/Tldraw/Tldraw
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
rodzic
cb118ef712
commit
88ee4e9993
|
@ -188,8 +188,6 @@ export interface BoundsSnapPoint {
|
||||||
export class Box {
|
export class Box {
|
||||||
constructor(x?: number, y?: number, w?: number, h?: number);
|
constructor(x?: number, y?: number, w?: number, h?: number);
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static AroundPoint(point: VecLike, n: number): Box;
|
|
||||||
// (undocumented)
|
|
||||||
get aspectRatio(): number;
|
get aspectRatio(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get center(): Vec;
|
get center(): Vec;
|
||||||
|
@ -757,7 +755,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getShapeClipPath(shape: TLShape | TLShapeId): string | undefined;
|
getShapeClipPath(shape: TLShape | TLShapeId): string | undefined;
|
||||||
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T;
|
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T;
|
||||||
getShapeHandles<T extends TLShape>(shape: T | T['id']): TLHandle[] | undefined;
|
getShapeHandles<T extends TLShape>(shape: T | T['id']): TLHandle[] | undefined;
|
||||||
getShapeIdsInsideBounds(bounds: Box): TLShapeId[];
|
|
||||||
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat;
|
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat;
|
||||||
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
|
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
|
||||||
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
||||||
|
@ -769,7 +766,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
hitInside?: boolean | undefined;
|
hitInside?: boolean | undefined;
|
||||||
margin?: number | undefined;
|
margin?: number | undefined;
|
||||||
}): TLShape[];
|
}): TLShape[];
|
||||||
getShapesInsideBounds(bounds: Box): TLShape[];
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getShapeStyleIfExists<T>(shape: TLShape, style: StyleProp<T>): T | undefined;
|
getShapeStyleIfExists<T>(shape: TLShape, style: StyleProp<T>): T | undefined;
|
||||||
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>;
|
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>;
|
||||||
|
|
|
@ -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",
|
"kind": "Property",
|
||||||
"canonicalReference": "@tldraw/editor!Box#aspectRatio:member",
|
"canonicalReference": "@tldraw/editor!Box#aspectRatio:member",
|
||||||
|
@ -12800,60 +12734,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getShapeHandles"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getShapeLocalTransform:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getShapeLocalTransform:member(1)",
|
||||||
|
@ -13357,60 +13237,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getShapesAtPoint"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getShapeStyleIfExists:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getShapeStyleIfExists:member(1)",
|
||||||
|
|
|
@ -59,8 +59,7 @@
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"nanoid": "4.0.2",
|
"nanoid": "4.0.2"
|
||||||
"rbush": "^3.0.1"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
@ -73,7 +72,6 @@
|
||||||
"@types/benchmark": "^2.1.2",
|
"@types/benchmark": "^2.1.2",
|
||||||
"@types/lodash.throttle": "^4.1.7",
|
"@types/lodash.throttle": "^4.1.7",
|
||||||
"@types/lodash.uniq": "^4.5.7",
|
"@types/lodash.uniq": "^4.5.7",
|
||||||
"@types/rbush": "^3.0.3",
|
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@types/wicg-file-system-access": "^2020.9.5",
|
"@types/wicg-file-system-access": "^2020.9.5",
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
|
|
|
@ -100,9 +100,9 @@ import { getReorderingShapesChanges } from '../utils/reorderShapes'
|
||||||
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
||||||
import { uniqueId } from '../utils/uniqueId'
|
import { uniqueId } from '../utils/uniqueId'
|
||||||
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
||||||
|
import { notVisibleShapes } from './derivations/notVisibleShapes'
|
||||||
import { parentsToChildren } from './derivations/parentsToChildren'
|
import { parentsToChildren } from './derivations/parentsToChildren'
|
||||||
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
||||||
import { SpatialIndex } from './derivations/spatialIndex'
|
|
||||||
import { getSvgJsx } from './getSvgJsx'
|
import { getSvgJsx } from './getSvgJsx'
|
||||||
import { ClickManager } from './managers/ClickManager'
|
import { ClickManager } from './managers/ClickManager'
|
||||||
import { EnvironmentManager } from './managers/EnvironmentManager'
|
import { EnvironmentManager } from './managers/EnvironmentManager'
|
||||||
|
@ -589,7 +589,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
)
|
)
|
||||||
this._parentIdsToChildIds = parentsToChildren(this.store)
|
this._parentIdsToChildIds = parentsToChildren(this.store)
|
||||||
|
|
||||||
this._spatialIndex = new SpatialIndex(this)
|
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.store.listen((changes) => {
|
this.store.listen((changes) => {
|
||||||
this.emit('change', changes)
|
this.emit('change', changes)
|
||||||
|
@ -4199,8 +4198,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
|
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @internal */
|
@computed
|
||||||
private readonly _spatialIndex: SpatialIndex
|
private _notVisibleShapes() {
|
||||||
|
return notVisibleShapes(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get culled shapes.
|
* Get culled shapes.
|
||||||
|
@ -4209,7 +4210,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
@computed
|
@computed
|
||||||
getCulledShapes() {
|
getCulledShapes() {
|
||||||
const notVisibleShapes = this._spatialIndex.getNotVisibleShapes()
|
const notVisibleShapes = this._notVisibleShapes().get()
|
||||||
const selectedShapeIds = this.getSelectedShapeIds()
|
const selectedShapeIds = this.getSelectedShapeIds()
|
||||||
const editingId = this.getEditingShapeId()
|
const editingId = this.getEditingShapeId()
|
||||||
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
|
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
|
||||||
|
@ -4224,31 +4225,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return culledShapes
|
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).
|
* 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.
|
* @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 {
|
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined {
|
||||||
const shapesCloseToPoint = new Set(
|
|
||||||
this.getShapeIdsInsideBounds(Box.AroundPoint(point, HIT_TEST_MARGIN))
|
|
||||||
)
|
|
||||||
const selectedShapeIds = this.getSelectedShapeIds()
|
const selectedShapeIds = this.getSelectedShapeIds()
|
||||||
|
|
||||||
return this.getCurrentPageShapesSorted()
|
return this.getCurrentPageShapesSorted()
|
||||||
.filter(
|
.filter((shape) => shape.type !== 'group' && selectedShapeIds.includes(shape.id))
|
||||||
(shape) =>
|
|
||||||
shape.type !== 'group' &&
|
|
||||||
shapesCloseToPoint.has(shape.id) &&
|
|
||||||
selectedShapeIds.includes(shape.id)
|
|
||||||
)
|
|
||||||
.reverse() // findlast
|
.reverse() // findlast
|
||||||
.find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }))
|
.find((shape) => this.isPointInShape(shape, point, { hitInside: true, margin: 0 }))
|
||||||
}
|
}
|
||||||
|
@ -4329,15 +4296,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
let inMarginClosestToEdgeDistance = Infinity
|
let inMarginClosestToEdgeDistance = Infinity
|
||||||
let inMarginClosestToEdgeHit: TLShape | null = null
|
let inMarginClosestToEdgeHit: TLShape | null = null
|
||||||
|
|
||||||
const shapesCloseToPoint = new Set(
|
|
||||||
this.getShapeIdsInsideBounds(Box.AroundPoint(point, HIT_TEST_MARGIN))
|
|
||||||
)
|
|
||||||
const shapesToCheck = (
|
const shapesToCheck = (
|
||||||
opts.renderingOnly
|
opts.renderingOnly
|
||||||
? this.getCurrentPageRenderingShapesSorted()
|
? this.getCurrentPageRenderingShapesSorted()
|
||||||
: this.getCurrentPageShapesSorted()
|
: this.getCurrentPageShapesSorted()
|
||||||
).filter((shape) => {
|
).filter((shape) => {
|
||||||
if (!shapesCloseToPoint.has(shape.id)) return
|
|
||||||
if (this.isShapeOfType(shape, 'group')) return false
|
if (this.isShapeOfType(shape, 'group')) return false
|
||||||
const pageMask = this.getShapeMask(shape)
|
const pageMask = this.getShapeMask(shape)
|
||||||
if (pageMask && !pointInPolygon(point, pageMask)) return false
|
if (pageMask && !pointInPolygon(point, pageMask)) return false
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -376,10 +376,6 @@ export class Box {
|
||||||
return new Box(minX, minY, maxX - minX, maxY - minY)
|
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) {
|
static Expand(A: Box, B: Box) {
|
||||||
const minX = Math.min(B.minX, A.minX)
|
const minX = Math.min(B.minX, A.minX)
|
||||||
const minY = Math.min(B.minY, A.minY)
|
const minY = Math.min(B.minY, A.minY)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
HIT_TEST_MARGIN,
|
HIT_TEST_MARGIN,
|
||||||
StateNode,
|
StateNode,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
|
@ -81,8 +80,9 @@ export class Erasing extends StateNode {
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const { editor, excludedShapeIds } = this
|
const { editor, excludedShapeIds } = this
|
||||||
const erasingShapeIds = this.editor.getErasingShapeIds()
|
const erasingShapeIds = editor.getErasingShapeIds()
|
||||||
const zoomLevel = this.editor.getZoomLevel()
|
const zoomLevel = editor.getZoomLevel()
|
||||||
|
const currentPageShapes = editor.getCurrentPageShapes()
|
||||||
const {
|
const {
|
||||||
inputs: { currentPagePoint, previousPagePoint },
|
inputs: { currentPagePoint, previousPagePoint },
|
||||||
} = editor
|
} = editor
|
||||||
|
@ -92,11 +92,8 @@ export class Erasing extends StateNode {
|
||||||
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
||||||
const minDist = HIT_TEST_MARGIN / zoomLevel
|
const minDist = HIT_TEST_MARGIN / zoomLevel
|
||||||
|
|
||||||
const shapesNearPoint = this.editor.getShapesInsideBounds(
|
for (const shape of currentPageShapes) {
|
||||||
Box.FromPoints([currentPagePoint, previousPagePoint]).expandBy(minDist)
|
if (editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
||||||
)
|
|
||||||
for (const shape of shapesNearPoint) {
|
|
||||||
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
|
||||||
|
|
||||||
// Avoid testing masked shapes, unless the pointer is inside the mask
|
// Avoid testing masked shapes, unless the pointer is inside the mask
|
||||||
const pageMask = editor.getShapeMask(shape.id)
|
const pageMask = editor.getShapeMask(shape.id)
|
||||||
|
|
|
@ -125,13 +125,12 @@ export class Brushing extends StateNode {
|
||||||
pageTransform: Mat | undefined,
|
pageTransform: Mat | undefined,
|
||||||
localCorners: Vec[]
|
localCorners: Vec[]
|
||||||
|
|
||||||
|
const currentPageShapes = editor.getCurrentPageShapes()
|
||||||
const currentPageId = editor.getCurrentPageId()
|
const currentPageId = editor.getCurrentPageId()
|
||||||
|
|
||||||
const shapesInsideBounds = this.editor.getShapesInsideBounds(brush)
|
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
||||||
testAllShapes: for (let i = 0, n = shapesInsideBounds.length; i < n; i++) {
|
shape = currentPageShapes[i]
|
||||||
shape = shapesInsideBounds[i]
|
if (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes
|
||||||
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
|
||||||
if (results.has(shape.id)) continue testAllShapes
|
|
||||||
|
|
||||||
pageBounds = editor.getShapePageBounds(shape)
|
pageBounds = editor.getShapePageBounds(shape)
|
||||||
if (!pageBounds) continue testAllShapes
|
if (!pageBounds) continue testAllShapes
|
||||||
|
@ -177,7 +176,7 @@ export class Brushing extends StateNode {
|
||||||
|
|
||||||
const current = editor.getSelectedShapeIds()
|
const current = editor.getSelectedShapeIds()
|
||||||
if (current.length !== results.size || current.some((id) => !results.has(id))) {
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
StateNode,
|
StateNode,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
|
@ -84,6 +83,8 @@ export class ScribbleBrushing extends StateNode {
|
||||||
|
|
||||||
private updateScribbleSelection(addPoint: boolean) {
|
private updateScribbleSelection(addPoint: boolean) {
|
||||||
const { editor } = this
|
const { editor } = this
|
||||||
|
// const zoomLevel = this.editor.getZoomLevel()
|
||||||
|
const currentPageShapes = this.editor.getCurrentPageShapes()
|
||||||
const {
|
const {
|
||||||
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
@ -94,9 +95,7 @@ export class ScribbleBrushing extends StateNode {
|
||||||
this.pushPointToScribble()
|
this.pushPointToScribble()
|
||||||
}
|
}
|
||||||
|
|
||||||
const shapes = this.editor.getShapesInsideBounds(
|
const shapes = currentPageShapes
|
||||||
Box.FromPoints([previousPagePoint, currentPagePoint])
|
|
||||||
)
|
|
||||||
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
|
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
|
||||||
|
|
||||||
const minDist = 0 // HIT_TEST_MARGIN / zoomLevel
|
const minDist = 0 // HIT_TEST_MARGIN / zoomLevel
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, PageRecordType, TLShapeId, createShapeId } from '@tldraw/editor'
|
import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
import { TL } from './test-jsx'
|
import { TL } from './test-jsx'
|
||||||
|
|
||||||
|
@ -130,14 +130,8 @@ it('correctly calculates the culled shapes when adding and deleting shapes', ()
|
||||||
const culledShapesIncremental = editor.getCulledShapes()
|
const culledShapesIncremental = editor.getCulledShapes()
|
||||||
|
|
||||||
// force full refresh
|
// force full refresh
|
||||||
const currentPage = editor.getCurrentPageId()
|
editor.pan({ x: -1, y: 0 })
|
||||||
const id = PageRecordType.createId('page2')
|
editor.pan({ x: 1, y: 0 })
|
||||||
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)
|
|
||||||
|
|
||||||
const culledShapeFromScratch = editor.getCulledShapes()
|
const culledShapeFromScratch = editor.getCulledShapes()
|
||||||
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
|
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
|
||||||
|
|
|
@ -1612,16 +1612,16 @@ describe('shift brushes to add to the selection', () => {
|
||||||
editor.keyDown('Shift')
|
editor.keyDown('Shift')
|
||||||
editor.pointerDown()
|
editor.pointerDown()
|
||||||
editor.pointerMove(1, 1)
|
editor.pointerMove(1, 1)
|
||||||
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
|
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
|
||||||
editor.keyUp('Shift')
|
editor.keyUp('Shift')
|
||||||
// there's a timer here—we should keep the shift mode until the timer expires
|
// 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)
|
jest.advanceTimersByTime(500)
|
||||||
// once the timer expires, we should be back in regular mode
|
// once the timer expires, we should be back in regular mode
|
||||||
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
|
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
|
||||||
editor.keyDown('Shift')
|
editor.keyDown('Shift')
|
||||||
// there's no timer on key down, so go right into shift mode again
|
// 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])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
25
yarn.lock
25
yarn.lock
|
@ -7489,7 +7489,6 @@ __metadata:
|
||||||
"@types/core-js": "npm:^2.5.5"
|
"@types/core-js": "npm:^2.5.5"
|
||||||
"@types/lodash.throttle": "npm:^4.1.7"
|
"@types/lodash.throttle": "npm:^4.1.7"
|
||||||
"@types/lodash.uniq": "npm:^4.5.7"
|
"@types/lodash.uniq": "npm:^4.5.7"
|
||||||
"@types/rbush": "npm:^3.0.3"
|
|
||||||
"@types/react-test-renderer": "npm:^18.0.0"
|
"@types/react-test-renderer": "npm:^18.0.0"
|
||||||
"@types/wicg-file-system-access": "npm:^2020.9.5"
|
"@types/wicg-file-system-access": "npm:^2020.9.5"
|
||||||
"@use-gesture/react": "npm:^10.2.27"
|
"@use-gesture/react": "npm:^10.2.27"
|
||||||
|
@ -7506,7 +7505,6 @@ __metadata:
|
||||||
lodash.throttle: "npm:^4.1.1"
|
lodash.throttle: "npm:^4.1.1"
|
||||||
lodash.uniq: "npm:^4.5.0"
|
lodash.uniq: "npm:^4.5.0"
|
||||||
nanoid: "npm:4.0.2"
|
nanoid: "npm:4.0.2"
|
||||||
rbush: "npm:^3.0.1"
|
|
||||||
react-test-renderer: "npm:^18.2.0"
|
react-test-renderer: "npm:^18.2.0"
|
||||||
resize-observer-polyfill: "npm:^1.5.1"
|
resize-observer-polyfill: "npm:^1.5.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -8333,13 +8331,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.18":
|
||||||
version: 18.2.18
|
version: 18.2.18
|
||||||
resolution: "@types/react-dom@npm:18.2.18"
|
resolution: "@types/react-dom@npm:18.2.18"
|
||||||
|
@ -21079,13 +21070,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"raf@npm:^3.4.1":
|
||||||
version: 3.4.1
|
version: 3.4.1
|
||||||
resolution: "raf@npm:3.4.1"
|
resolution: "raf@npm:3.4.1"
|
||||||
|
@ -21123,15 +21107,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"rc@npm:^1.2.7, rc@npm:^1.2.8, rc@npm:~1.2.7":
|
||||||
version: 1.2.8
|
version: 1.2.8
|
||||||
resolution: "rc@npm:1.2.8"
|
resolution: "rc@npm:1.2.8"
|
||||||
|
|
Ładowanie…
Reference in New Issue