diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 2310050a1..c553624ad 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -676,7 +676,7 @@ export class Editor extends EventEmitter { getCrashingError(): unknown; getCroppingShapeId(): null | TLShapeId; // (undocumented) - getCulledShapes(): Map; + getCulledShapes(): Set; getCurrentPage(): TLPage; getCurrentPageBounds(): Box | undefined; getCurrentPageId(): TLPageId; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 229ca2636..5788b96b0 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10295,8 +10295,8 @@ }, { "kind": "Reference", - "text": "Map", - "canonicalReference": "!Map:interface" + "text": "Set", + "canonicalReference": "!Set:interface" }, { "kind": "Content", @@ -10309,16 +10309,7 @@ }, { "kind": "Content", - "text": ", " - }, - { - "kind": "Reference", - "text": "Box", - "canonicalReference": "@tldraw/editor!Box:class" - }, - { - "kind": "Content", - "text": " | undefined>" + "text": ">" }, { "kind": "Content", @@ -10328,7 +10319,7 @@ "isStatic": false, "returnTypeTokenRange": { "startIndex": 1, - "endIndex": 7 + "endIndex": 5 }, "releaseTag": "Public", "isProtected": false, diff --git a/packages/editor/src/lib/components/CulledShapes.tsx b/packages/editor/src/lib/components/CulledShapes.tsx index 88e7d15f3..09fd40adc 100644 --- a/packages/editor/src/lib/components/CulledShapes.tsx +++ b/packages/editor/src/lib/components/CulledShapes.tsx @@ -1,5 +1,4 @@ import { computed, react } from '@tldraw/state' -import { measureCbDuration } from '@tldraw/utils' import { useEffect, useRef } from 'react' import { useEditor } from '../hooks/useEditor' import { useIsDarkMode } from '../hooks/useIsDarkMode' @@ -111,32 +110,32 @@ export function CulledShapes() { } = webGl const shapeVertices = computed('shape vertices', function calculateCulledShapeVertices() { - const results: number[] = [] + return [] + // const results: number[] = [] + // return measureCbDuration('vertices', () => { + // editor.getCulledShapes().forEach((maskedPageBounds) => { + // if (maskedPageBounds) { + // results.push( + // // triangle 1 + // maskedPageBounds.minX, + // maskedPageBounds.minY, + // maskedPageBounds.minX, + // maskedPageBounds.maxY, + // maskedPageBounds.maxX, + // maskedPageBounds.maxY, + // // triangle 2 + // maskedPageBounds.minX, + // maskedPageBounds.minY, + // maskedPageBounds.maxX, + // maskedPageBounds.minY, + // maskedPageBounds.maxX, + // maskedPageBounds.maxY + // ) + // } + // }) - return measureCbDuration('vertices', () => { - editor.getCulledShapes().forEach((maskedPageBounds) => { - if (maskedPageBounds) { - results.push( - // triangle 1 - maskedPageBounds.minX, - maskedPageBounds.minY, - maskedPageBounds.minX, - maskedPageBounds.maxY, - maskedPageBounds.maxX, - maskedPageBounds.maxY, - // triangle 2 - maskedPageBounds.minX, - maskedPageBounds.minY, - maskedPageBounds.maxX, - maskedPageBounds.minY, - maskedPageBounds.maxX, - maskedPageBounds.maxY - ) - } - }) - - return results - }) + // return results + // }) }) return react('render culled shapes ', function renderCulledShapes() { diff --git a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx index 3f89197b1..2a19d02d0 100644 --- a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx +++ b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx @@ -20,7 +20,6 @@ import { toDomPrecision } from '../../primitives/utils' import { debugFlags } from '../../utils/debug-flags' import { setStyleProperty } from '../../utils/dom' import { nearestMultiple } from '../../utils/nearestMultiple' -import { CulledShapes } from '../CulledShapes' import { GeometryDebuggingView } from '../GeometryDebuggingView' import { LiveCollaborators } from '../LiveCollaborators' import { Shape } from '../Shape' @@ -97,9 +96,9 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) { )} -
+ {/*
-
+
*/}
{ this.getCurrentPageId() ) this._parentIdsToChildIds = parentsToChildren(this.store) + this._culledShapes = culledShapes(this) this.disposables.add( this.store.listen((changes) => { @@ -4247,127 +4249,18 @@ export class Editor extends EventEmitter { return this.isShapeOrAncestorLocked(this.getShapeParent(shape)) } - private _getShapeCullingInfo( - id: TLShapeId, - selectedShapeIds: TLShapeId[], - editingId: TLShapeId | null, - renderingBoundsExpanded: Box - ): { isCulled: false } | { isCulled: true; maskedPageBounds: Box | undefined } { - if (editingId === id) return { isCulled: false } - - const maskedPageBounds = this.getShapeMaskedPageBounds(id) - // if the shape is fully outside of its parent's clipping bounds... - if (maskedPageBounds === undefined) return { isCulled: true, maskedPageBounds: undefined } - - // We don't cull selected shapes - if (selectedShapeIds.includes(id)) return { isCulled: false } - // the shape is outside of the expanded viewport bounds... - - const isCulled = !renderingBoundsExpanded.includes(maskedPageBounds) - return isCulled ? { isCulled, maskedPageBounds } : { isCulled } - } - - getCulledShapes(): Map { - return this._getCulledShapes().get() - } - @computed - private _getCulledShapes() { - const isCullingOffScreenShapes = Number.isFinite(this.renderingBoundsMargin) - - const shapeHistory = this.store.query.filterHistory('shape') - let lastPageId: TLPageId | null = null - let prevRenderingBoundsExpanded: Box - - function fromScratch(editor: Editor): Map { - const renderingBoundsExpanded = editor.getRenderingBoundsExpanded() - prevRenderingBoundsExpanded = renderingBoundsExpanded.clone() - lastPageId = editor.getCurrentPageId() - const shapes = editor.getCurrentPageShapeIds() - const culledShapes = new Map() - const selectedShapeIds = editor.getSelectedShapeIds() - const editingId = editor.getEditingShapeId() - shapes.forEach((id) => { - const ci = editor._getShapeCullingInfo( - id, - selectedShapeIds, - editingId, - renderingBoundsExpanded - ) - if (ci.isCulled) { - culledShapes.set(id, ci.maskedPageBounds) - } - }) - return culledShapes - } - return computed>( - 'getCulledShapes', - (prevValue, lastComputedEpoch) => { - if (!isCullingOffScreenShapes) return new Map() - - if (isUninitialized(prevValue)) { - return fromScratch(this) - } - const diff = shapeHistory.getDiffSince(lastComputedEpoch) - - if (diff === RESET_VALUE) { - return fromScratch(this) - } - - const currentPageId = this.getCurrentPageId() - if (lastPageId !== currentPageId) { - return fromScratch(this) - } - const renderingBoundsExpanded = this.getRenderingBoundsExpanded() - if ( - !prevRenderingBoundsExpanded || - !renderingBoundsExpanded.equals(prevRenderingBoundsExpanded) - ) { - return fromScratch(this) - } - const selectedShapeIds = this.getSelectedShapeIds() - const editingId = this.getEditingShapeId() - const nextValue = new Map(prevValue) - let isDirty = false - const checkShapeCullingInfo = (id: TLShapeId) => { - const ci = this._getShapeCullingInfo( - id, - selectedShapeIds, - editingId, - renderingBoundsExpanded - ) - if (ci.isCulled && !prevValue.has(id)) { - nextValue.set(id, ci.maskedPageBounds) - isDirty = true - } - } - for (const changes of diff) { - for (const record of Object.values(changes.added)) { - if (isShape(record)) { - checkShapeCullingInfo(record.id) - } - } - - for (const [_from, to] of Object.values(changes.updated)) { - if (isShape(to)) { - checkShapeCullingInfo(to.id) - } - } - for (const id of Object.keys(changes.removed)) { - if (isShapeId(id)) { - const hasBeenDeleted = nextValue.delete(id) - if (hasBeenDeleted) { - isDirty = true - } - } - } - } - - return isDirty ? nextValue : prevValue - } - ) + getCulledShapes(): Set { + return this._culledShapes.get() } + /** + * A cache of parents to children. + * + * @internal + */ + private readonly _culledShapes: ReturnType + /** * The bounds of the current page (the common bounds of all of the shapes on the page). * diff --git a/packages/editor/src/lib/editor/derivations/culledShapes.ts b/packages/editor/src/lib/editor/derivations/culledShapes.ts new file mode 100644 index 000000000..ae7224aef --- /dev/null +++ b/packages/editor/src/lib/editor/derivations/culledShapes.ts @@ -0,0 +1,101 @@ +import { computed, isUninitialized, RESET_VALUE } from '@tldraw/state' +import { isShape, isShapeId, TLPageId, TLShapeId } from '@tldraw/tlschema' +import { Box } from '../../primitives/Box' +import { Editor } from '../Editor' + +export function culledShapes(editor: Editor) { + function getShapeCullingInfo( + id: TLShapeId, + selectedShapeIds: TLShapeId[], + editingId: TLShapeId | null, + viewportPageBounds: Box + ): boolean { + if (editingId === id) return false + const maskedPageBounds = editor.getShapeMaskedPageBounds(id) + // if the shape is fully outside of its parent's clipping bounds... + if (maskedPageBounds === undefined) return true + // We don't cull selected shapes + if (selectedShapeIds.includes(id)) return false + // the shape is outside of the expanded viewport bounds... + return !viewportPageBounds.includes(maskedPageBounds) + } + + const isCullingOffScreenShapes = Number.isFinite(editor.renderingBoundsMargin) + + const shapeHistory = editor.store.query.filterHistory('shape') + let lastPageId: TLPageId | null = null + let prevBounds: Box + + function fromScratch(editor: Editor): Set { + const bounds = editor.getViewportPageBounds() + prevBounds = bounds.clone() + lastPageId = editor.getCurrentPageId() + const shapes = editor.getCurrentPageShapeIds() + const culledShapes = new Set() + const selectedShapeIds = editor.getSelectedShapeIds() + const editingId = editor.getEditingShapeId() + shapes.forEach((id) => { + if (getShapeCullingInfo(id, selectedShapeIds, editingId, bounds)) { + culledShapes.add(id) + } + }) + return culledShapes + } + + return computed>('getCulledShapes', (prevValue, lastComputedEpoch) => { + if (!isCullingOffScreenShapes) return new Set() + + 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 renderingBoundsExpanded = editor.getViewportPageBounds() + if (!prevBounds || !renderingBoundsExpanded.equals(prevBounds)) { + return fromScratch(editor) + } + const selectedShapeIds = editor.getSelectedShapeIds() + const editingId = editor.getEditingShapeId() + const nextValue = new Set(prevValue) + let isDirty = false + const checkShapeCullingInfo = (id: TLShapeId) => { + if ( + getShapeCullingInfo(id, selectedShapeIds, editingId, renderingBoundsExpanded) && + !prevValue.has(id) + ) { + nextValue.add(id) + isDirty = true + } + } + for (const changes of diff) { + for (const record of Object.values(changes.added)) { + if (isShape(record)) { + checkShapeCullingInfo(record.id) + } + } + + for (const [_from, to] of Object.values(changes.updated)) { + if (isShape(to)) { + checkShapeCullingInfo(to.id) + } + } + for (const id of Object.keys(changes.removed)) { + if (isShapeId(id)) { + if (nextValue.delete(id)) { + isDirty = true + } + } + } + } + + return isDirty ? nextValue : prevValue + }) +}