[fix] camera culling (#1602)

This PR restores camera culling behavior and includes a 500ms forced
render while the camera is moving to prevent weird long pan behavior.

It:
- removes `CameraManager`
- adds `cameraState` to editor

### Change Type

- [x] `major` — Breaking change

### Release Notes

- [editor] Adds `Editor.cameraState`
- Adds smart culling to make panning and zooming more smooth
This commit is contained in:
Steve Ruiz 2023-06-16 14:02:38 +01:00 committed by GitHub
parent 271d0088e9
commit bdd8913af3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 38 deletions

View file

@ -371,6 +371,7 @@ export class Editor extends EventEmitter<TLEventMap> {
bringToFront(ids?: TLShapeId[]): this;
get brush(): Box2dModel | null;
get camera(): TLCamera;
get cameraState(): "idle" | "moving";
cancel(): this;
cancelDoubleClick(): void;
get canMoveCamera(): boolean;

View file

@ -110,3 +110,6 @@ export const INTERNAL_POINTER_IDS = {
/** @internal */
export const CAMERA_MOVING_TIMEOUT = 64
/** @internal */
export const CAMERA_MAX_RENDERING_INTERVAL = 620

View file

@ -84,6 +84,8 @@ import { checkShapesAndAddCore } from '../config/defaultShapes'
import { AnyTLShapeInfo } from '../config/defineShape'
import {
ANIMATION_MEDIUM_MS,
CAMERA_MAX_RENDERING_INTERVAL,
CAMERA_MOVING_TIMEOUT,
COARSE_DRAG_DISTANCE,
COLLABORATOR_TIMEOUT,
DEFAULT_ANIMATION_OPTIONS,
@ -115,7 +117,6 @@ import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
import { ActiveAreaManager, getActiveAreaScreenSpace } from './managers/ActiveAreaManager'
import { CameraManager } from './managers/CameraManager'
import { ClickManager } from './managers/ClickManager'
import { DprManager } from './managers/DprManager'
import { ExternalContentManager, TLExternalContent } from './managers/ExternalContentManager'
@ -382,9 +383,6 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */
private _dprManager = new DprManager(this)
/** @internal */
private _cameraManager = new CameraManager(this)
/** @internal */
private _activeAreaManager = new ActiveAreaManager(this)
@ -2353,7 +2351,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
this._cameraManager.tick()
this._tickCameraState()
this.updateRenderingBounds()
const { editingId } = this
@ -2454,6 +2452,55 @@ export class Editor extends EventEmitter<TLEventMap> {
/* -------- Rendering Shapes / rendering Bounds ------- */
private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
/**
* Whether the camera is moving or idle.
*
* @public
*/
@computed get cameraState() {
return this._cameraState.value
}
// Camera state does two things: first, it allows us to subscribe to whether
// the camera is moving or not; and second, it allows us to update the rendering
// shapes on the canvas. Changing the rendering shapes may cause shapes to
// unmount / remount in the DOM, which is expensive; and computing visibility is
// also expensive in large projects. For this reason, we use a second bounding
// box just for rendering, and we only update after the camera stops moving.
private _cameraStateTimeoutRemaining = 0
private _lastUpdateRenderingBoundsTimestamp = Date.now()
private _decayCameraStateTimeout = (elapsed: number) => {
this._cameraStateTimeoutRemaining -= elapsed
if (this._cameraStateTimeoutRemaining <= 0) {
this.off('tick', this._decayCameraStateTimeout)
this._cameraState.set('idle')
this.updateRenderingBounds()
}
}
private _tickCameraState = () => {
// always reset the timeout
this._cameraStateTimeoutRemaining = CAMERA_MOVING_TIMEOUT
const now = Date.now()
// If the state is idle, then start the tick
if (this._cameraState.__unsafe__getWithoutCapture() === 'idle') {
this._lastUpdateRenderingBoundsTimestamp = now // don't render right away
this._cameraState.set('moving')
this.on('tick', this._decayCameraStateTimeout)
} else {
if (now - this._lastUpdateRenderingBoundsTimestamp > CAMERA_MAX_RENDERING_INTERVAL) {
this.updateRenderingBounds()
}
}
}
private computeUnorderedRenderingShapes(
ids: TLParentId[],
{
@ -8051,7 +8098,7 @@ export class Editor extends EventEmitter<TLEventMap> {
isPen: this.isPenMode ?? false,
})
this._cameraManager.tick()
this._tickCameraState()
})
return this

View file

@ -1,32 +0,0 @@
import { atom } from 'signia'
import { Editor } from '../Editor'
const CAMERA_SETTLE_TIMEOUT = 12
export class CameraManager {
constructor(public editor: Editor) {}
state = atom('camera state', 'idle' as 'idle' | 'moving')
private timeoutRemaining = 0
private decay = (elapsed: number) => {
this.timeoutRemaining -= elapsed
if (this.timeoutRemaining <= 0) {
this.state.set('idle')
this.editor.off('tick', this.decay)
this.editor.updateRenderingBounds()
}
}
tick = () => {
// always reset the timeout
this.timeoutRemaining = CAMERA_SETTLE_TIMEOUT
// If the state is idle, then start the tick
if (this.state.__unsafe__getWithoutCapture() === 'idle') {
this.state.set('moving')
this.editor.on('tick', this.decay)
}
}
}