[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;
|
bringToFront(ids?: TLShapeId[]): this;
|
||||||
get brush(): Box2dModel | null;
|
get brush(): Box2dModel | null;
|
||||||
get camera(): TLCamera;
|
get camera(): TLCamera;
|
||||||
|
get cameraState(): "idle" | "moving";
|
||||||
cancel(): this;
|
cancel(): this;
|
||||||
cancelDoubleClick(): void;
|
cancelDoubleClick(): void;
|
||||||
get canMoveCamera(): boolean;
|
get canMoveCamera(): boolean;
|
||||||
|
|
|
@ -110,3 +110,6 @@ export const INTERNAL_POINTER_IDS = {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const CAMERA_MOVING_TIMEOUT = 64
|
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 { AnyTLShapeInfo } from '../config/defineShape'
|
||||||
import {
|
import {
|
||||||
ANIMATION_MEDIUM_MS,
|
ANIMATION_MEDIUM_MS,
|
||||||
|
CAMERA_MAX_RENDERING_INTERVAL,
|
||||||
|
CAMERA_MOVING_TIMEOUT,
|
||||||
COARSE_DRAG_DISTANCE,
|
COARSE_DRAG_DISTANCE,
|
||||||
COLLABORATOR_TIMEOUT,
|
COLLABORATOR_TIMEOUT,
|
||||||
DEFAULT_ANIMATION_OPTIONS,
|
DEFAULT_ANIMATION_OPTIONS,
|
||||||
|
@ -115,7 +117,6 @@ import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
||||||
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
|
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
|
||||||
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
||||||
import { ActiveAreaManager, getActiveAreaScreenSpace } from './managers/ActiveAreaManager'
|
import { ActiveAreaManager, getActiveAreaScreenSpace } from './managers/ActiveAreaManager'
|
||||||
import { CameraManager } from './managers/CameraManager'
|
|
||||||
import { ClickManager } from './managers/ClickManager'
|
import { ClickManager } from './managers/ClickManager'
|
||||||
import { DprManager } from './managers/DprManager'
|
import { DprManager } from './managers/DprManager'
|
||||||
import { ExternalContentManager, TLExternalContent } from './managers/ExternalContentManager'
|
import { ExternalContentManager, TLExternalContent } from './managers/ExternalContentManager'
|
||||||
|
@ -382,9 +383,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private _dprManager = new DprManager(this)
|
private _dprManager = new DprManager(this)
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
private _cameraManager = new CameraManager(this)
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private _activeAreaManager = new ActiveAreaManager(this)
|
private _activeAreaManager = new ActiveAreaManager(this)
|
||||||
|
|
||||||
|
@ -2353,7 +2351,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._cameraManager.tick()
|
this._tickCameraState()
|
||||||
this.updateRenderingBounds()
|
this.updateRenderingBounds()
|
||||||
|
|
||||||
const { editingId } = this
|
const { editingId } = this
|
||||||
|
@ -2454,6 +2452,55 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
/* -------- Rendering Shapes / rendering Bounds ------- */
|
/* -------- 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(
|
private computeUnorderedRenderingShapes(
|
||||||
ids: TLParentId[],
|
ids: TLParentId[],
|
||||||
{
|
{
|
||||||
|
@ -8051,7 +8098,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
isPen: this.isPenMode ?? false,
|
isPen: this.isPenMode ?? false,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._cameraManager.tick()
|
this._tickCameraState()
|
||||||
})
|
})
|
||||||
|
|
||||||
return this
|
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