move camera state out, trim unused methods

pull/3282/head
Steve Ruiz 2024-04-22 16:37:32 +01:00
rodzic 849f94851d
commit 0e50cd4f36
11 zmienionych plików z 130 dodań i 430 usunięć

Wyświetl plik

@ -127,11 +127,21 @@ The `behavior` property defines which logic should be used when calculating the
There are several `Editor` methods available for controlling the camera.
| Method | Description |
| [Editor#zoomIn](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| Method | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
| [Editor#setCamera](?) | Moves the camera to the provided coordinates. |
| [Editor#zoomIn](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomOut](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomToFit](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomToBounds](?) | Moves the camera to fit the given bounding box. |
| [Editor#zoomToSelection](?) | Moves the camera to fit the current selection. |
| [Editor#zoomToUser](?) | Moves the camera to center on a user's cursor. |
| [Editor#resetZoom](?) | Resets the zoom to 100% or to the `initialZoom` zoom level. |
| [Editor#centerOnPoint](?) | Centers the camera on the given point. |
| [Editor#pan](?) | Moves the camera's x and y position. |
| [Editor#slideCamera](?) | Slides the camera in a given direction with a certain velocity and friction. |
| [Editor#stopCameraAnimation](?) | Stops any camera animation. |
The [Editor#zoomOut](?) method zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information.
# Camera state
The [Editor#zoomToContent](?) method moves the camera to fit the current page content.
The [Editor#zoomToBounds](?) method moves the camera to fit the current page content.
The camera may be in two states, `idle` or `moving`. You can get the current camera state with [Editor#getCameraState](?).

Wyświetl plik

@ -68,5 +68,5 @@ function createDemoShapes(editor: Editor) {
},
}))
)
.zoomToContent({ animation: { duration: 0 } })
.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -58,5 +58,5 @@ function createDemoShapes(editor: Editor) {
})),
])
editor.zoomToContent({ animation: { duration: 0 } })
editor.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -44,5 +44,5 @@ function createDemoShapes(editor: Editor) {
},
},
])
.zoomToContent({ animation: { duration: 0 } })
.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -50,7 +50,6 @@ const ZOOM_EVENT = {
'reset-zoom': 'resetZoom',
'zoom-to-fit': 'zoomToFit',
'zoom-to-selection': 'zoomToSelection',
'zoom-to-content': 'zoomToContent',
}
export function getCodeSnippet(name: string, data: any) {
@ -136,15 +135,11 @@ if (updates.length > 0) {
} else if (name === 'fit-frame-to-content') {
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
if (name === 'zoom-to-content') {
codeSnippet = 'editor.zoomToContent()'
} else {
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
? 'editor.getViewportScreenCenter(), '
: ''
}{ duration: 320 })`
}
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
? 'editor.getViewportScreenCenter(), '
: ''
}{ duration: 320 })`
} else if (name.startsWith('toggle-')) {
if (name === 'toggle-lock') {
codeSnippet = `editor.toggleLock(editor.getSelectedShapeIds())`

Wyświetl plik

@ -619,6 +619,8 @@ export class Editor extends EventEmitter<TLEventMap> {
batch(fn: () => void): this;
bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
// (undocumented)
readonly cameraState: CameraStateManager;
cancel(): this;
cancelDoubleClick(): void;
// @internal (undocumented)
@ -695,7 +697,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getBaseZoom(): number;
getCamera(): TLCamera;
getCameraOptions(): TLCameraOptions;
getCameraState(): "idle" | "moving";
getCanRedo(): boolean;
getCanUndo(): boolean;
getCollaborators(): TLInstancePresence[];
@ -869,7 +870,6 @@ export class Editor extends EventEmitter<TLEventMap> {
pageToScreen(point: VecLike): Vec;
pageToViewport(point: VecLike): Vec;
pan(offset: VecLike, opts?: TLCameraMoveOptions): this;
panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this;
popFocusedGroupId(): this;
putContentOntoCurrentPage(content: TLContent, options?: {
point?: VecLike;
@ -960,10 +960,8 @@ export class Editor extends EventEmitter<TLEventMap> {
inset?: number;
targetZoom?: number;
} & TLCameraMoveOptions): this;
zoomToContent(opts?: TLCameraMoveOptions): this;
zoomToFit(opts?: TLCameraMoveOptions): this;
zoomToSelection(opts?: TLCameraMoveOptions): this;
zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this;
zoomToUser(userId: string, opts?: TLCameraMoveOptions): this;
}

Wyświetl plik

@ -7937,6 +7937,37 @@
"isAbstract": false,
"name": "bringToFront"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!Editor#cameraState:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "readonly cameraState: "
},
{
"kind": "Reference",
"text": "CameraStateManager",
"canonicalReference": "@tldraw/editor!~CameraStateManager:class"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": true,
"isOptional": false,
"releaseTag": "Public",
"name": "cameraState",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#cancel:member(1)",
@ -9937,37 +9968,6 @@
"isAbstract": false,
"name": "getCameraOptions"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCameraState:member(1)",
"docComment": "/**\n * Whether the camera is moving or idle.\n *\n * @example\n * ```ts\n * editor.getCameraState()\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getCameraState(): "
},
{
"kind": "Content",
"text": "\"idle\" | \"moving\""
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCameraState"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCanRedo:member(1)",
@ -15766,76 +15766,6 @@
"isAbstract": false,
"name": "pan"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#panZoomIntoView:member(1)",
"docComment": "/**\n * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.\n *\n * @param ids - The ids of the shapes to pan and zoom into view.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.panZoomIntoView([myShape.id])\n * editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "panZoomIntoView(ids: "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 6,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "ids",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 4,
"endIndex": 5
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "panZoomIntoView"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#popFocusedGroupId:member(1)",
@ -19911,55 +19841,6 @@
"isAbstract": false,
"name": "zoomToBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToContent:member(1)",
"docComment": "/**\n * Move the camera to the nearest content.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToContent()\n * editor.zoomToContent({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "zoomToContent(opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "zoomToContent"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)",
@ -20058,72 +19939,6 @@
"isAbstract": false,
"name": "zoomToSelection"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToShape:member(1)",
"docComment": "/**\n * Animate the camera to a shape.\n *\n * @param shapeId - The id of the shape to animate to.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToShape(myShape.id)\n * editor.zoomToShape(myShape.id, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "zoomToShape(shapeId: "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "shapeId",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "zoomToShape"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)",

Wyświetl plik

@ -649,7 +649,7 @@ function InFrontOfTheCanvasWrapper() {
function MovingCameraHitTestBlocker() {
const editor = useEditor()
const cameraState = useValue('camera state', () => editor.getCameraState(), [editor])
const cameraState = useValue('camera state', () => editor.cameraState.getCameraState(), [editor])
return (
<div

Wyświetl plik

@ -66,7 +66,6 @@ import { TLUser, createTLUser } from '../config/createTLUser'
import { checkShapesAndAddCore } from '../config/defaultShapes'
import {
ANIMATION_MEDIUM_MS,
CAMERA_MOVING_TIMEOUT,
CAMERA_SLIDE_FRICTION,
COARSE_DRAG_DISTANCE,
COLLABORATOR_IDLE_TIMEOUT,
@ -108,6 +107,7 @@ import { notVisibleShapes } from './derivations/notVisibleShapes'
import { parentsToChildren } from './derivations/parentsToChildren'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
import { getSvgJsx } from './getSvgJsx'
import { CameraStateManager } from './managers/CameraStateManager'
import { ClickManager } from './managers/ClickManager'
import { EnvironmentManager } from './managers/EnvironmentManager'
import { HistoryManager } from './managers/HistoryManager'
@ -218,6 +218,7 @@ export class Editor extends EventEmitter<TLEventMap> {
this.textMeasure = new TextManager(this)
this._tickManager = new TickManager(this)
this.cameraState = new CameraStateManager(this)
class NewRoot extends RootState {
static override initial = initialState ?? ''
@ -675,6 +676,8 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
readonly user: UserPreferencesManager
readonly cameraState: CameraStateManager
/**
* A helper for measuring text.
*
@ -696,6 +699,13 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
readonly scribbles: ScribbleManager
/**
* A manager for side effects and correct state enforcement. See {@link SideEffectManager} for details.
*
* @public
*/
readonly sideEffects: SideEffectManager<this>
/**
* The current HTML element containing the editor.
*
@ -708,13 +718,6 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
getContainer: () => HTMLElement
/**
* A manager for side effects and correct state enforcement. See {@link SideEffectManager} for details.
*
* @public
*/
readonly sideEffects: SideEffectManager<this>
/**
* Dispose the editor.
*
@ -2373,7 +2376,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
this._tickCameraState()
this.cameraState.tick()
})
return this
@ -2446,26 +2449,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/**
* Move the camera to the nearest content.
*
* @example
* ```ts
* editor.zoomToContent()
* editor.zoomToContent({ animation: { duration: 200 } })
* ```
*
* @param opts - The camera move options.
*
* @public
*/
zoomToContent(opts: TLCameraMoveOptions = { animation: { duration: 220 } }): this {
const bounds = this.getSelectionPageBounds() ?? this.getCurrentPageBounds()
if (!bounds) return this
this.zoomToBounds(bounds, { targetZoom: Math.min(1, this.getZoomLevel()), ...opts })
return this
}
/**
* Zoom the camera to fit the current page's content in the viewport.
*
@ -2640,66 +2623,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/**
* Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.
*
* @example
* ```ts
* editor.panZoomIntoView([myShape.id])
* editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } })
* ```
*
* @param ids - The ids of the shapes to pan and zoom into view.
* @param opts - The camera move options.
*
* @public
*/
panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this {
if (this.getCameraOptions().isLocked) return this
if (ids.length <= 0) return this
const selectionBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
const viewportPageBounds = this.getViewportPageBounds()
if (viewportPageBounds.h < selectionBounds.h || viewportPageBounds.w < selectionBounds.w) {
this.zoomToBounds(selectionBounds, { targetZoom: this.getCamera().z, ...opts })
return this
} else {
const insetViewport = this.getViewportPageBounds()
.clone()
.expandBy(-32 / this.getZoomLevel())
let offsetX = 0
let offsetY = 0
if (insetViewport.maxY < selectionBounds.maxY) {
// off bottom
offsetY = insetViewport.maxY - selectionBounds.maxY
} else if (insetViewport.minY > selectionBounds.minY) {
// off top
offsetY = insetViewport.minY - selectionBounds.minY
} else {
// inside y-bounds
}
if (insetViewport.maxX < selectionBounds.maxX) {
// off right
offsetX = insetViewport.maxX - selectionBounds.maxX
} else if (insetViewport.minX > selectionBounds.minX) {
// off left
offsetX = insetViewport.minX - selectionBounds.minX
} else {
// inside x-bounds
}
const { x: cx, y: cy, z: cz } = this.getCamera()
this.setCamera(new Vec(cx + offsetX, cy + offsetY, cz), opts)
}
return this
}
/**
* Zoom the camera to fit a bounding box (in the current page space).
*
@ -2985,53 +2908,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/**
* Animate the camera to a shape.
*
* @example
* ```ts
* editor.zoomToShape(myShape.id)
* editor.zoomToShape(myShape.id, { animation: { duration: 200 } })
* ```
*
* @param shapeId - The id of the shape to animate to.
* @param opts - The camera move options.
*
* @public
*/
zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this {
if (this.getCameraOptions().isLocked) return this
const activeArea = this.getViewportScreenBounds().clone().expandBy(-32)
const viewportAspectRatio = activeArea.width / activeArea.height
const shapePageBounds = this.getShapePageBounds(shapeId)
if (!shapePageBounds) return this
const shapeAspectRatio = shapePageBounds.width / shapePageBounds.height
const targetViewportPage = shapePageBounds.clone()
const z = shapePageBounds.width / activeArea.width
targetViewportPage.width += (activeArea.minX + activeArea.maxX) * z
targetViewportPage.height += (activeArea.minY + activeArea.maxY) * z
targetViewportPage.x -= activeArea.minX * z
targetViewportPage.y -= activeArea.minY * z
if (shapeAspectRatio > viewportAspectRatio) {
targetViewportPage.height = shapePageBounds.width / viewportAspectRatio
targetViewportPage.y -= (targetViewportPage.height - shapePageBounds.height) / 2
} else {
targetViewportPage.width = shapePageBounds.height * viewportAspectRatio
targetViewportPage.x -= (targetViewportPage.width - shapePageBounds.width) / 2
}
this._animateToViewport(targetViewportPage, opts)
return this
}
// Viewport
/** @internal */
@ -3101,7 +2977,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
this._tickCameraState()
this.cameraState.tick()
this.updateRenderingBounds()
return this
@ -3403,58 +3279,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
// Camera state
private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
/**
* Whether the camera is moving or idle.
*
* @example
* ```ts
* editor.getCameraState()
* ```
*
* @public
*/
getCameraState() {
return this._cameraState.get()
}
// 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)
}
}
/** @internal */
getUnorderedRenderingShapes(
// The rendering state. We use this method both for rendering, which

Wyświetl plik

@ -0,0 +1,53 @@
import { atom } from '@tldraw/state'
import { CAMERA_MOVING_TIMEOUT } from '../../constants'
import { Editor } from '../Editor'
export class CameraStateManager {
constructor(public editor: Editor) {}
// Camera state
// 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 _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
/**
* Whether the camera is moving or idle.
*
* @example
* ```ts
* editor.getCameraState()
* ```
*
* @public
*/
getCameraState() {
return this._cameraState.get()
}
private _cameraStateTimeoutRemaining = 0
private _decayCameraStateTimeout = (elapsed: number) => {
this._cameraStateTimeoutRemaining -= elapsed
if (this._cameraStateTimeoutRemaining <= 0) {
this.editor.off('tick', this._decayCameraStateTimeout)
this._cameraState.set('idle')
this.editor.updateRenderingBounds()
}
}
public tick = () => {
// always reset the timeout
this._cameraStateTimeoutRemaining = CAMERA_MOVING_TIMEOUT
// If the state is idle, then start the tick
if (this._cameraState.__unsafe__getWithoutCapture() === 'idle') {
this._cameraState.set('moving')
this.editor.on('tick', this._decayCameraStateTimeout)
}
}
}

Wyświetl plik

@ -1297,7 +1297,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('zoom-to-content', { source })
editor.zoomToContent()
const bounds = editor.getSelectionPageBounds() ?? editor.getCurrentPageBounds()
if (!bounds) return
editor.zoomToBounds(bounds, {
targetZoom: Math.min(1, editor.getZoomLevel()),
animation: { duration: 220 },
})
},
},
{