From 4fdc67b8a3aca9cc49b2ec095f5cc552b5e57fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 28 Mar 2024 20:08:37 +0100 Subject: [PATCH] WIP --- packages/editor/package.json | 1 + packages/editor/src/lib/constants.ts | 2 +- packages/editor/src/lib/editor/Editor.ts | 75 +++++++++++++++++------- yarn.lock | 8 +++ 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/editor/package.json b/packages/editor/package.json index 6b5d134aa..6679994d9 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -45,6 +45,7 @@ "lint": "yarn run -T tsx ../../scripts/lint.ts" }, "dependencies": { + "@timohausmann/quadtree-ts": "^2.2.2", "@tldraw/state": "workspace:*", "@tldraw/store": "workspace:*", "@tldraw/tlschema": "workspace:*", diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index fbdb79814..31b73ad29 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -1,7 +1,7 @@ import { EASINGS } from './primitives/easings' /** @internal */ -export const MAX_SHAPES_PER_PAGE = 2000 +export const MAX_SHAPES_PER_PAGE = 10000 /** @internal */ export const MAX_PAGES = 40 diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 5f5705495..806079e47 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -1,4 +1,5 @@ -import { EMPTY_ARRAY, atom, computed, transact } from '@tldraw/state' +import { Quadtree, Rectangle } from '@timohausmann/quadtree-ts' +import { EMPTY_ARRAY, atom, computed, transact, whyAmIRunning } from '@tldraw/state' import { ComputedCache, RecordType, StoreSnapshot } from '@tldraw/store' import { CameraRecordType, @@ -3087,6 +3088,38 @@ export class Editor extends EventEmitter { } } + quadTree = new Quadtree({ width: 40000, height: 40000, x: -20000, y: -20000 }) + shapesInTheTree = new Set() + quadRectsById = new Map>() + + @computed quadTree2() { + const shapes = this.getCurrentPageShapes() + whyAmIRunning() + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i] + if (!this.shapesInTheTree.has(shape.id)) { + const maskedPageBounds = this.getShapeMaskedPageBounds(shape.id) + if (!maskedPageBounds) continue + + let rect = this.quadRectsById.get(shape.id) + if (!rect) { + rect = new Rectangle({ + x: maskedPageBounds?.x, + y: maskedPageBounds?.y, + width: maskedPageBounds?.width, + height: maskedPageBounds?.height, + data: shape.id, + }) + } + + this.quadRectsById.set(shape.id, rect) + this.quadTree.insert(rect) + this.shapesInTheTree.add(shape.id) + } + } + return 'bla' + } + /** @internal */ getUnorderedRenderingShapes( // The rendering state. We use this method both for rendering, which @@ -3112,20 +3145,24 @@ export class Editor extends EventEmitter { backgroundIndex: number opacity: number isCulled: boolean - maskedPageBounds: Box | undefined }[] = [] let nextIndex = MAX_SHAPES_PER_PAGE * 2 let nextBackgroundIndex = MAX_SHAPES_PER_PAGE // We only really need these if we're using editor state, but that's ok - const editingShapeId = this.getEditingShapeId() - const selectedShapeIds = this.getSelectedShapeIds() const erasingShapeIds = this.getErasingShapeIds() const renderingBoundsExpanded = this.getRenderingBoundsExpanded() - // If renderingBoundsMargin is set to Infinity, then we won't cull offscreen shapes - const isCullingOffScreenShapes = Number.isFinite(this.renderingBoundsMargin) + const shapesInView = this.quadTree.retrieve( + new Rectangle({ + x: renderingBoundsExpanded.x, + y: renderingBoundsExpanded.y, + width: renderingBoundsExpanded.width, + height: renderingBoundsExpanded.height, + }) + ) + const idsOfShapesInView = new Set(shapesInView.map((s) => (s as any).data)) const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => { const shape = this.getShape(id) @@ -3135,27 +3172,14 @@ export class Editor extends EventEmitter { let isCulled = false let isShapeErasing = false const util = this.getShapeUtil(shape) - const maskedPageBounds = this.getShapeMaskedPageBounds(id) if (useEditorState) { isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id) if (isShapeErasing) { opacity *= 0.32 } - - isCulled = - isCullingOffScreenShapes && - // only cull shapes that allow unmounting, i.e. not stateful components - util.canUnmount(shape) && - // never cull editingg shapes - editingShapeId !== id && - // if the shape is fully outside of its parent's clipping bounds... - (maskedPageBounds === undefined || - // ...or if the shape is outside of the expanded viewport bounds... - (!renderingBoundsExpanded.includes(maskedPageBounds) && - // ...and if it's not selected... then cull it - !selectedShapeIds.includes(id))) } + isCulled = !idsOfShapesInView.has(id) renderingShapes.push({ id, @@ -3165,7 +3189,6 @@ export class Editor extends EventEmitter { backgroundIndex: nextBackgroundIndex, opacity, isCulled, - maskedPageBounds, }) nextIndex += 1 @@ -3193,6 +3216,7 @@ export class Editor extends EventEmitter { // If we're using editor state, then we're only interested in on-screen shapes. // If we're not using the editor state, then we're interested in ALL shapes, even those from other pages. const pages = useEditorState ? [this.getCurrentPage()] : this.getPages() + // const ids = shapesInView.map((s) => (s as any).data) for (const page of pages) { for (const childId of this.getSortedChildIdsForParent(page.id)) { addShapeById(childId, 1, false) @@ -3208,7 +3232,11 @@ export class Editor extends EventEmitter { * @public */ @computed getRenderingShapes() { + let now = Date.now() + const bl = this.quadTree2() const renderingShapes = this.getUnorderedRenderingShapes(true) + console.log('get rendering shapes in ', Date.now() - now) + now = Date.now() // Its IMPORTANT that the result be sorted by id AND include the index // that the shape should be displayed at. Steve, this is the past you @@ -3220,7 +3248,10 @@ export class Editor extends EventEmitter { // drain. By always sorting by 'id' we keep the shapes always in the // same order; but we later use index to set the element's 'z-index' // to change the "rendered" position in z-space. - return renderingShapes.sort(sortById) + const result = renderingShapes.sort(sortById) + console.log('sorted shapes in ', Date.now() - now) + + return result } /** diff --git a/yarn.lock b/yarn.lock index c81a8d937..49b356a03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7172,6 +7172,13 @@ __metadata: languageName: node linkType: hard +"@timohausmann/quadtree-ts@npm:^2.2.2": + version: 2.2.2 + resolution: "@timohausmann/quadtree-ts@npm:2.2.2" + checksum: d5d02bd5ad5042c976a6443c9415ec4fe401ca3c8853d0886357f65f3a01af2a07a6ea05bb24b57753b1a4c3ea03d90f81b11a90bcc1074ef25eef76fc338e92 + languageName: node + linkType: hard + "@tldraw/assets@workspace:*, @tldraw/assets@workspace:packages/assets": version: 0.0.0-use.local resolution: "@tldraw/assets@workspace:packages/assets" @@ -7281,6 +7288,7 @@ __metadata: "@peculiar/webcrypto": "npm:^1.4.0" "@testing-library/jest-dom": "npm:^5.16.5" "@testing-library/react": "npm:^14.0.0" + "@timohausmann/quadtree-ts": "npm:^2.2.2" "@tldraw/state": "workspace:*" "@tldraw/store": "workspace:*" "@tldraw/tlschema": "workspace:*"