[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:
parent
271d0088e9
commit
bdd8913af3
4 changed files with 57 additions and 38 deletions
|
@ -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;
|
||||
|
|
|
@ -110,3 +110,6 @@ export const INTERNAL_POINTER_IDS = {
|
|||
|
||||
/** @internal */
|
||||
export const CAMERA_MOVING_TIMEOUT = 64
|
||||
|
||||
/** @internal */
|
||||
export const CAMERA_MAX_RENDERING_INTERVAL = 620
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue