Improve edge scrolling (#3950)
This PR: - moves the edge scrolling logic into a manager - adds a new Editor option for `edgeScrollDelay` - adds a new Editor option for `edgeScrollEaseDuration` When in a state that would trigger an edge scroll, a delay is added before the scrolling starts. When scrolling does start, it is eased in by a certain duration. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features ### Test Plan 1. Drag shapes, resize, or drag select to the edge of the screen 2. The screen should move - [x] Unit Tests ### Release Notes - Add a delay and easing to edge scrolling. --------- Co-authored-by: Mitja Bezenšek <mitja.bezensek@gmail.com>
This commit is contained in:
parent
ccc673b5af
commit
6c7b8febbf
12 changed files with 294 additions and 97 deletions
|
@ -679,8 +679,10 @@ export const defaultTldrawOptions: {
|
||||||
readonly defaultSvgPadding: 32;
|
readonly defaultSvgPadding: 32;
|
||||||
readonly doubleClickDurationMs: 450;
|
readonly doubleClickDurationMs: 450;
|
||||||
readonly dragDistanceSquared: 16;
|
readonly dragDistanceSquared: 16;
|
||||||
|
readonly edgeScrollDelay: 200;
|
||||||
readonly edgeScrollDistance: 8;
|
readonly edgeScrollDistance: 8;
|
||||||
readonly edgeScrollSpeed: 20;
|
readonly edgeScrollEaseDuration: 200;
|
||||||
|
readonly edgeScrollSpeed: 25;
|
||||||
readonly flattenImageBoundsExpand: 64;
|
readonly flattenImageBoundsExpand: 64;
|
||||||
readonly flattenImageBoundsPadding: 16;
|
readonly flattenImageBoundsPadding: 16;
|
||||||
readonly followChaseViewportSnap: 2;
|
readonly followChaseViewportSnap: 2;
|
||||||
|
@ -780,6 +782,14 @@ export class Edge2d extends Geometry2d {
|
||||||
ul: number;
|
ul: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export class EdgeScrollManager {
|
||||||
|
constructor(editor: Editor);
|
||||||
|
// (undocumented)
|
||||||
|
editor: Editor;
|
||||||
|
updateEdgeScrolling(elapsed: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Editor extends EventEmitter<TLEventMap> {
|
export class Editor extends EventEmitter<TLEventMap> {
|
||||||
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, assetOptions, initialState, autoFocus, inferDarkMode, options, }: TLEditorOptions);
|
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, assetOptions, initialState, autoFocus, inferDarkMode, options, }: TLEditorOptions);
|
||||||
|
@ -883,6 +893,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
distributeShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
distributeShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
||||||
duplicatePage(page: TLPage | TLPageId, createId?: TLPageId): this;
|
duplicatePage(page: TLPage | TLPageId, createId?: TLPageId): this;
|
||||||
duplicateShapes(shapes: TLShape[] | TLShapeId[], offset?: VecLike): this;
|
duplicateShapes(shapes: TLShape[] | TLShapeId[], offset?: VecLike): this;
|
||||||
|
edgeScrollManager: EdgeScrollManager;
|
||||||
readonly environment: EnvironmentManager;
|
readonly environment: EnvironmentManager;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
externalAssetContentHandlers: {
|
externalAssetContentHandlers: {
|
||||||
|
@ -1754,9 +1765,6 @@ export interface MatModel {
|
||||||
f: number;
|
f: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public
|
|
||||||
export function moveCameraWhenCloseToEdge(editor: Editor): void;
|
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function normalizeWheel(event: React.WheelEvent<HTMLElement> | WheelEvent): {
|
export function normalizeWheel(event: React.WheelEvent<HTMLElement> | WheelEvent): {
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -2573,8 +2581,12 @@ export interface TldrawOptions {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly dragDistanceSquared: number;
|
readonly dragDistanceSquared: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
readonly edgeScrollDelay: number;
|
||||||
|
// (undocumented)
|
||||||
readonly edgeScrollDistance: number;
|
readonly edgeScrollDistance: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
readonly edgeScrollEaseDuration: number;
|
||||||
|
// (undocumented)
|
||||||
readonly edgeScrollSpeed: number;
|
readonly edgeScrollSpeed: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly flattenImageBoundsExpand: number;
|
readonly flattenImageBoundsExpand: number;
|
||||||
|
|
|
@ -136,6 +136,7 @@ export {
|
||||||
type TLBindingUtilConstructor,
|
type TLBindingUtilConstructor,
|
||||||
} from './lib/editor/bindings/BindingUtil'
|
} from './lib/editor/bindings/BindingUtil'
|
||||||
export { ClickManager, type TLClickState } from './lib/editor/managers/ClickManager'
|
export { ClickManager, type TLClickState } from './lib/editor/managers/ClickManager'
|
||||||
|
export { EdgeScrollManager } from './lib/editor/managers/EdgeScrollManager'
|
||||||
export { EnvironmentManager } from './lib/editor/managers/EnvironmentManager'
|
export { EnvironmentManager } from './lib/editor/managers/EnvironmentManager'
|
||||||
export { HistoryManager } from './lib/editor/managers/HistoryManager'
|
export { HistoryManager } from './lib/editor/managers/HistoryManager'
|
||||||
export { ScribbleManager, type ScribbleItem } from './lib/editor/managers/ScribbleManager'
|
export { ScribbleManager, type ScribbleItem } from './lib/editor/managers/ScribbleManager'
|
||||||
|
@ -361,7 +362,6 @@ export {
|
||||||
setPointerCapture,
|
setPointerCapture,
|
||||||
stopEventPropagation,
|
stopEventPropagation,
|
||||||
} from './lib/utils/dom'
|
} from './lib/utils/dom'
|
||||||
export { moveCameraWhenCloseToEdge } from './lib/utils/edgeScrolling'
|
|
||||||
export { getIncrementedName } from './lib/utils/getIncrementedName'
|
export { getIncrementedName } from './lib/utils/getIncrementedName'
|
||||||
export { getPointerInfo } from './lib/utils/getPointerInfo'
|
export { getPointerInfo } from './lib/utils/getPointerInfo'
|
||||||
export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
|
export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
|
||||||
|
|
|
@ -125,6 +125,7 @@ import { parentsToChildren } from './derivations/parentsToChildren'
|
||||||
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
|
||||||
import { getSvgJsx } from './getSvgJsx'
|
import { getSvgJsx } from './getSvgJsx'
|
||||||
import { ClickManager } from './managers/ClickManager'
|
import { ClickManager } from './managers/ClickManager'
|
||||||
|
import { EdgeScrollManager } from './managers/EdgeScrollManager'
|
||||||
import { EnvironmentManager } from './managers/EnvironmentManager'
|
import { EnvironmentManager } from './managers/EnvironmentManager'
|
||||||
import { FocusManager } from './managers/FocusManager'
|
import { FocusManager } from './managers/FocusManager'
|
||||||
import { HistoryManager } from './managers/HistoryManager'
|
import { HistoryManager } from './managers/HistoryManager'
|
||||||
|
@ -692,6 +693,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.root.enter(undefined, 'initial')
|
this.root.enter(undefined, 'initial')
|
||||||
|
|
||||||
|
this.edgeScrollManager = new EdgeScrollManager(this)
|
||||||
this.focusManager = new FocusManager(this, autoFocus)
|
this.focusManager = new FocusManager(this, autoFocus)
|
||||||
this.disposables.add(this.focusManager.dispose.bind(this.focusManager))
|
this.disposables.add(this.focusManager.dispose.bind(this.focusManager))
|
||||||
|
|
||||||
|
@ -791,6 +793,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
readonly sideEffects: StoreSideEffects<TLRecord>
|
readonly sideEffects: StoreSideEffects<TLRecord>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager for moving the camera when the mouse is at the edge of the screen.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
edgeScrollManager: EdgeScrollManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A manager for ensuring correct focus. See FocusManager for details.
|
* A manager for ensuring correct focus. See FocusManager for details.
|
||||||
*
|
*
|
||||||
|
|
128
packages/editor/src/lib/editor/managers/EdgeScrollManager.ts
Normal file
128
packages/editor/src/lib/editor/managers/EdgeScrollManager.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { Vec } from '../../primitives/Vec'
|
||||||
|
import { EASINGS } from '../../primitives/easings'
|
||||||
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export class EdgeScrollManager {
|
||||||
|
constructor(public editor: Editor) {}
|
||||||
|
|
||||||
|
private _isEdgeScrolling = false
|
||||||
|
private _edgeScrollDuration = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the camera position when the mouse is close to the edge of the screen.
|
||||||
|
* Run this on every tick when in a state where edge scrolling is enabled.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
updateEdgeScrolling(elapsed: number) {
|
||||||
|
const { editor } = this
|
||||||
|
const edgeScrollProximityFactor = this.getEdgeScroll()
|
||||||
|
if (edgeScrollProximityFactor.x === 0 && edgeScrollProximityFactor.y === 0) {
|
||||||
|
if (this._isEdgeScrolling) {
|
||||||
|
this._isEdgeScrolling = false
|
||||||
|
this._edgeScrollDuration = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!this._isEdgeScrolling) {
|
||||||
|
this._isEdgeScrolling = true
|
||||||
|
this._edgeScrollDuration = 0
|
||||||
|
}
|
||||||
|
this._edgeScrollDuration += elapsed
|
||||||
|
if (this._edgeScrollDuration > editor.options.edgeScrollDelay) {
|
||||||
|
const eased =
|
||||||
|
editor.options.edgeScrollEaseDuration > 0
|
||||||
|
? EASINGS.easeInCubic(
|
||||||
|
Math.min(
|
||||||
|
1,
|
||||||
|
this._edgeScrollDuration /
|
||||||
|
(editor.options.edgeScrollDelay + editor.options.edgeScrollEaseDuration)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 1
|
||||||
|
this.moveCameraWhenCloseToEdge({
|
||||||
|
x: edgeScrollProximityFactor.x * eased,
|
||||||
|
y: edgeScrollProximityFactor.y * eased,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get the scroll proximity factor for a given position.
|
||||||
|
* @param position - The mouse position on the axis.
|
||||||
|
* @param dimension - The component dimension on the axis.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private getEdgeProximityFactors(
|
||||||
|
position: number,
|
||||||
|
dimension: number,
|
||||||
|
isCoarse: boolean,
|
||||||
|
insetStart: boolean,
|
||||||
|
insetEnd: boolean
|
||||||
|
) {
|
||||||
|
const { editor } = this
|
||||||
|
const dist = editor.options.edgeScrollDistance
|
||||||
|
const pw = isCoarse ? editor.options.coarsePointerWidth : 0 // pointer width
|
||||||
|
const pMin = position - pw
|
||||||
|
const pMax = position + pw
|
||||||
|
const min = insetStart ? 0 : dist
|
||||||
|
const max = insetEnd ? dimension : dimension - dist
|
||||||
|
if (pMin < min) {
|
||||||
|
return Math.min(1, (min - pMin) / dist)
|
||||||
|
} else if (pMax > max) {
|
||||||
|
return -Math.min(1, (pMax - max) / dist)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEdgeScroll() {
|
||||||
|
const { editor } = this
|
||||||
|
const {
|
||||||
|
inputs: {
|
||||||
|
currentScreenPoint: { x, y },
|
||||||
|
},
|
||||||
|
} = editor
|
||||||
|
const screenBounds = editor.getViewportScreenBounds()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isCoarsePointer,
|
||||||
|
insets: [t, r, b, l],
|
||||||
|
} = editor.getInstanceState()
|
||||||
|
const proximityFactorX = this.getEdgeProximityFactors(x, screenBounds.w, isCoarsePointer, l, r)
|
||||||
|
const proximityFactorY = this.getEdgeProximityFactors(y, screenBounds.h, isCoarsePointer, t, b)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: proximityFactorX,
|
||||||
|
y: proximityFactorY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the camera when the mouse is close to the edge of the screen.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
|
||||||
|
const { editor } = this
|
||||||
|
if (!editor.inputs.isDragging || editor.inputs.isPanning || editor.getCameraOptions().isLocked)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (proximityFactor.x === 0 && proximityFactor.y === 0) return
|
||||||
|
|
||||||
|
const screenBounds = editor.getViewportScreenBounds()
|
||||||
|
|
||||||
|
// Determines how much the speed is affected by the screen size
|
||||||
|
const screenSizeFactorX = screenBounds.w < 1000 ? 0.612 : 1
|
||||||
|
const screenSizeFactorY = screenBounds.h < 1000 ? 0.612 : 1
|
||||||
|
|
||||||
|
// Determines the base speed of the scroll
|
||||||
|
const zoomLevel = editor.getZoomLevel()
|
||||||
|
const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed
|
||||||
|
const scrollDeltaX = (pxSpeed * proximityFactor.x * screenSizeFactorX) / zoomLevel
|
||||||
|
const scrollDeltaY = (pxSpeed * proximityFactor.y * screenSizeFactorY) / zoomLevel
|
||||||
|
|
||||||
|
// update the camera
|
||||||
|
const { x, y, z } = editor.getCamera()
|
||||||
|
editor.setCamera(new Vec(x + scrollDeltaX, y + scrollDeltaY, z))
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ export interface TldrawOptions {
|
||||||
readonly collaboratorCheckIntervalMs: number
|
readonly collaboratorCheckIntervalMs: number
|
||||||
readonly cameraMovingTimeoutMs: number
|
readonly cameraMovingTimeoutMs: number
|
||||||
readonly hitTestMargin: number
|
readonly hitTestMargin: number
|
||||||
|
readonly edgeScrollDelay: number
|
||||||
|
readonly edgeScrollEaseDuration: number
|
||||||
readonly edgeScrollSpeed: number
|
readonly edgeScrollSpeed: number
|
||||||
readonly edgeScrollDistance: number
|
readonly edgeScrollDistance: number
|
||||||
readonly coarsePointerWidth: number
|
readonly coarsePointerWidth: number
|
||||||
|
@ -73,7 +75,9 @@ export const defaultTldrawOptions = {
|
||||||
collaboratorCheckIntervalMs: 1200,
|
collaboratorCheckIntervalMs: 1200,
|
||||||
cameraMovingTimeoutMs: 64,
|
cameraMovingTimeoutMs: 64,
|
||||||
hitTestMargin: 8,
|
hitTestMargin: 8,
|
||||||
edgeScrollSpeed: 20,
|
edgeScrollDelay: 200,
|
||||||
|
edgeScrollEaseDuration: 200,
|
||||||
|
edgeScrollSpeed: 25,
|
||||||
edgeScrollDistance: 8,
|
edgeScrollDistance: 8,
|
||||||
coarsePointerWidth: 12,
|
coarsePointerWidth: 12,
|
||||||
coarseHandleRadius: 20,
|
coarseHandleRadius: 20,
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { Editor } from '../editor/Editor'
|
|
||||||
import { Vec } from '../primitives/Vec'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to get the scroll proximity factor for a given position.
|
|
||||||
* @param position - The mouse position on the axis.
|
|
||||||
* @param dimension - The component dimension on the axis.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
function getEdgeProximityFactor(
|
|
||||||
editor: Editor,
|
|
||||||
position: number,
|
|
||||||
dimension: number,
|
|
||||||
isCoarse: boolean,
|
|
||||||
insetStart: boolean,
|
|
||||||
insetEnd: boolean
|
|
||||||
) {
|
|
||||||
const dist = editor.options.edgeScrollDistance
|
|
||||||
const pw = isCoarse ? editor.options.coarsePointerWidth : 0 // pointer width
|
|
||||||
const pMin = position - pw
|
|
||||||
const pMax = position + pw
|
|
||||||
const min = insetStart ? 0 : dist
|
|
||||||
const max = insetEnd ? dimension : dimension - dist
|
|
||||||
if (pMin < min) {
|
|
||||||
return Math.min(1, (min - pMin) / dist)
|
|
||||||
} else if (pMax > max) {
|
|
||||||
return -Math.min(1, (pMax - max) / dist)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the camera when the mouse is close to the edge of the screen.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export function moveCameraWhenCloseToEdge(editor: Editor) {
|
|
||||||
if (!editor.inputs.isDragging || editor.inputs.isPanning || editor.getCameraOptions().isLocked)
|
|
||||||
return
|
|
||||||
|
|
||||||
const {
|
|
||||||
inputs: {
|
|
||||||
currentScreenPoint: { x, y },
|
|
||||||
},
|
|
||||||
} = editor
|
|
||||||
const zoomLevel = editor.getZoomLevel()
|
|
||||||
const screenBounds = editor.getViewportScreenBounds()
|
|
||||||
|
|
||||||
// Determines how much the speed is affected by the screen size
|
|
||||||
const screenSizeFactorX = screenBounds.w < 1000 ? 0.612 : 1
|
|
||||||
const screenSizeFactorY = screenBounds.h < 1000 ? 0.612 : 1
|
|
||||||
|
|
||||||
const {
|
|
||||||
isCoarsePointer,
|
|
||||||
insets: [t, r, b, l],
|
|
||||||
} = editor.getInstanceState()
|
|
||||||
const proximityFactorX = getEdgeProximityFactor(editor, x, screenBounds.w, isCoarsePointer, l, r)
|
|
||||||
const proximityFactorY = getEdgeProximityFactor(editor, y, screenBounds.h, isCoarsePointer, t, b)
|
|
||||||
|
|
||||||
if (proximityFactorX === 0 && proximityFactorY === 0) return
|
|
||||||
|
|
||||||
// Determines the base speed of the scroll
|
|
||||||
const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed
|
|
||||||
const scrollDeltaX = (pxSpeed * proximityFactorX * screenSizeFactorX) / zoomLevel
|
|
||||||
const scrollDeltaY = (pxSpeed * proximityFactorY * screenSizeFactorY) / zoomLevel
|
|
||||||
|
|
||||||
const camera = editor.getCamera()
|
|
||||||
|
|
||||||
editor.setCamera(new Vec(camera.x + scrollDeltaX, camera.y + scrollDeltaY, camera.z))
|
|
||||||
}
|
|
|
@ -12,8 +12,8 @@ import {
|
||||||
TLPointerEventInfo,
|
TLPointerEventInfo,
|
||||||
TLShape,
|
TLShape,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
|
TLTickEventInfo,
|
||||||
Vec,
|
Vec,
|
||||||
moveCameraWhenCloseToEdge,
|
|
||||||
pointInPolygon,
|
pointInPolygon,
|
||||||
polygonsIntersect,
|
polygonsIntersect,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
@ -62,8 +62,9 @@ export class Brushing extends StateNode {
|
||||||
this.editor.updateInstanceState({ brush: null })
|
this.editor.updateInstanceState({ brush: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTick = () => {
|
override onTick = ({ elapsed }: TLTickEventInfo) => {
|
||||||
moveCameraWhenCloseToEdge(this.editor)
|
const { editor } = this
|
||||||
|
editor.edgeScrollManager.updateEdgeScrolling(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove = () => {
|
override onPointerMove = () => {
|
||||||
|
|
|
@ -14,11 +14,11 @@ import {
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
TLTextShape,
|
TLTextShape,
|
||||||
|
TLTickEventInfo,
|
||||||
Vec,
|
Vec,
|
||||||
VecLike,
|
VecLike,
|
||||||
areAnglesCompatible,
|
areAnglesCompatible,
|
||||||
compact,
|
compact,
|
||||||
moveCameraWhenCloseToEdge,
|
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||||
|
|
||||||
|
@ -72,8 +72,9 @@ export class Resizing extends StateNode {
|
||||||
this.updateShapes()
|
this.updateShapes()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTick = () => {
|
override onTick = ({ elapsed }: TLTickEventInfo) => {
|
||||||
moveCameraWhenCloseToEdge(this.editor)
|
const { editor } = this
|
||||||
|
editor.edgeScrollManager.updateEdgeScrolling(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||||
|
|
|
@ -10,10 +10,10 @@ import {
|
||||||
TLPointerEventInfo,
|
TLPointerEventInfo,
|
||||||
TLShape,
|
TLShape,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
|
TLTickEventInfo,
|
||||||
Vec,
|
Vec,
|
||||||
compact,
|
compact,
|
||||||
isPageId,
|
isPageId,
|
||||||
moveCameraWhenCloseToEdge,
|
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import {
|
import {
|
||||||
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
|
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
|
||||||
|
@ -101,12 +101,13 @@ export class Translating extends StateNode {
|
||||||
this.dragAndDropManager.clear()
|
this.dragAndDropManager.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTick = () => {
|
override onTick = ({ elapsed }: TLTickEventInfo) => {
|
||||||
|
const { editor } = this
|
||||||
this.dragAndDropManager.updateDroppingNode(
|
this.dragAndDropManager.updateDroppingNode(
|
||||||
this.snapshot.movingShapes,
|
this.snapshot.movingShapes,
|
||||||
this.updateParentTransforms
|
this.updateParentTransforms
|
||||||
)
|
)
|
||||||
moveCameraWhenCloseToEdge(this.editor)
|
editor.edgeScrollManager.updateEdgeScrolling(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove = () => {
|
override onPointerMove = () => {
|
||||||
|
|
|
@ -4,7 +4,12 @@ import { TestEditor } from '../TestEditor'
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 0,
|
||||||
|
edgeScrollEaseDuration: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900))
|
editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -227,11 +232,13 @@ describe('CameraOptions.panSpeed', () => {
|
||||||
.forceTick()
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1.01 }) // 1 + 1
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1.01 }) // 1 + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Does not effect hand tool panning', () => {
|
it('Does not effect hand tool panning', () => {
|
||||||
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
||||||
editor.setCurrentTool('hand').pointerDown(0, 0).pointerMove(5, 10).forceTick()
|
editor.setCurrentTool('hand').pointerDown(0, 0).pointerMove(5, 10).forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Effects spacebar panning (2x)', () => {
|
it('Effects spacebar panning (2x)', () => {
|
||||||
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
||||||
editor
|
editor
|
||||||
|
@ -241,6 +248,7 @@ describe('CameraOptions.panSpeed', () => {
|
||||||
.forceTick()
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 10, y: 20, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 10, y: 20, z: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Effects spacebar panning (0.5x)', () => {
|
it('Effects spacebar panning (0.5x)', () => {
|
||||||
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 0.5 })
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 0.5 })
|
||||||
editor
|
editor
|
||||||
|
@ -250,6 +258,7 @@ describe('CameraOptions.panSpeed', () => {
|
||||||
.forceTick()
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 2.5, y: 5, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 2.5, y: 5, z: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Does not effect edge scroll panning', () => {
|
it('Does not effect edge scroll panning', () => {
|
||||||
const shapeId = createShapeId()
|
const shapeId = createShapeId()
|
||||||
const viewportScreenBounds = editor.getViewportScreenBounds()
|
const viewportScreenBounds = editor.getViewportScreenBounds()
|
||||||
|
@ -261,11 +270,12 @@ describe('CameraOptions.panSpeed', () => {
|
||||||
const shape = editor.getSelectedShapes()[0]
|
const shape = editor.getSelectedShapes()[0]
|
||||||
editor.selectNone()
|
editor.selectNone()
|
||||||
// Move shape far beyond bounds to trigger edge scrolling at maximum speed
|
// Move shape far beyond bounds to trigger edge scrolling at maximum speed
|
||||||
editor.pointerDown(shape.x, shape.y).pointerMove(-5000, -5000).forceTick()
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
editor.pointerDown(shape.x, shape.y).pointerMove(-5000, -5000)
|
||||||
// At maximum speed and a zoom level of 1, the camera should move by 40px per tick if the screen
|
// At maximum speed and a zoom level of 1, the camera should move by 40px per tick if the screen
|
||||||
// is wider than 1000 pixels, or by 40 * 0.612px if it is smaller.
|
// is wider than 1000 pixels, or by 40 * 0.612px if it is smaller.
|
||||||
const newX = viewportScreenBounds.w < 1000 ? 40 * 0.612 : 40
|
const newX = viewportScreenBounds.w < 1000 ? 25 * 0.612 : 25
|
||||||
const newY = viewportScreenBounds.h < 1000 ? 40 * 0.612 : 40
|
const newY = viewportScreenBounds.h < 1000 ? 25 * 0.612 : 25
|
||||||
expect(editor.getCamera()).toMatchObject({ x: newX, y: newY, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: newX, y: newY, z: 1 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,12 @@ const ids = {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 0,
|
||||||
|
edgeScrollEaseDuration: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
|
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1951,3 +1956,88 @@ it('Ignores locked shapes when hovering', () => {
|
||||||
editor.rightClick()
|
editor.rightClick()
|
||||||
expect(editor.getSelectedShapeIds()).toEqual([b.id])
|
expect(editor.getSelectedShapeIds()).toEqual([b.id])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Edge scrolling', () => {
|
||||||
|
it('moves the camera correctly when delay and duration are zero', () => {
|
||||||
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 0,
|
||||||
|
edgeScrollEaseDuration: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
|
||||||
|
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
||||||
|
|
||||||
|
editor.pointerMove(300, 300)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(0, 0)
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: editor.options.edgeScrollSpeed,
|
||||||
|
y: editor.options.edgeScrollSpeed,
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.forceTick()
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: editor.options.edgeScrollSpeed * 2,
|
||||||
|
y: editor.options.edgeScrollSpeed * 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves the camera correctly when delay is 16 and duration are zero', () => {
|
||||||
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 16,
|
||||||
|
edgeScrollEaseDuration: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
|
||||||
|
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
||||||
|
|
||||||
|
editor.pointerMove(300, 300)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(0, 0)
|
||||||
|
|
||||||
|
// one tick's length of delay
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.forceTick()
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: editor.options.edgeScrollSpeed,
|
||||||
|
y: editor.options.edgeScrollSpeed,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves the camera correctly when delay is 0 and duration is 32', () => {
|
||||||
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 0,
|
||||||
|
edgeScrollEaseDuration: 32,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
|
||||||
|
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
||||||
|
|
||||||
|
editor.pointerMove(300, 300)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(0, 0)
|
||||||
|
|
||||||
|
// one tick's length of delay
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: editor.options.edgeScrollSpeed * 0.125,
|
||||||
|
y: editor.options.edgeScrollSpeed * 0.125,
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.forceTick()
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchObject({
|
||||||
|
x: editor.options.edgeScrollSpeed * 1.125,
|
||||||
|
y: editor.options.edgeScrollSpeed * 1.125,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -40,7 +40,12 @@ const ids = {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
console.error = jest.fn()
|
console.error = jest.fn()
|
||||||
editor = new TestEditor()
|
editor = new TestEditor({
|
||||||
|
options: {
|
||||||
|
edgeScrollDelay: 0,
|
||||||
|
edgeScrollEaseDuration: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getNumSnapPoints = (snap: SnapIndicator): number => {
|
const getNumSnapPoints = (snap: SnapIndicator): number => {
|
||||||
|
@ -139,10 +144,10 @@ describe('When translating...', () => {
|
||||||
|
|
||||||
const before = editor.getShape<TLGeoShape>(ids.box1)!
|
const before = editor.getShape<TLGeoShape>(ids.box1)!
|
||||||
|
|
||||||
jest.advanceTimersByTime(100)
|
editor.forceTick()
|
||||||
editor
|
editor
|
||||||
// The change is bigger than expected because the camera moves
|
// The change is bigger than expected because the camera moves
|
||||||
.expectShapeToMatch({ id: ids.box1, x: -160, y: 10 })
|
.expectShapeToMatch({ id: ids.box1, x: -65, y: 10 })
|
||||||
// We'll continue moving in the x postion, but now we'll also move in the y position.
|
// We'll continue moving in the x postion, but now we'll also move in the y position.
|
||||||
// The speed in the y position is smaller since we are further away from the edge.
|
// The speed in the y position is smaller since we are further away from the edge.
|
||||||
.pointerMove(0, 25)
|
.pointerMove(0, 25)
|
||||||
|
@ -161,16 +166,21 @@ describe('When translating...', () => {
|
||||||
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
||||||
editor.pointerDown(50, 50, ids.box1).pointerMove(1080, 50)
|
editor.pointerDown(50, 50, ids.box1).pointerMove(1080, 50)
|
||||||
|
|
||||||
jest.advanceTimersByTime(100)
|
editor.forceTick()
|
||||||
|
editor.forceTick()
|
||||||
|
editor.forceTick()
|
||||||
editor
|
editor
|
||||||
// The change is bigger than expected because the camera moves
|
// The change is bigger than expected because the camera moves
|
||||||
.expectShapeToMatch({ id: ids.box1, x: 1160, y: 10 })
|
.expectShapeToMatch({ id: ids.box1, x: 1115, y: 10 })
|
||||||
.pointerMove(1080, 800)
|
.pointerMove(1080, 800)
|
||||||
jest.advanceTimersByTime(100)
|
|
||||||
|
editor.forceTick()
|
||||||
|
editor.forceTick()
|
||||||
|
editor.forceTick()
|
||||||
editor
|
editor
|
||||||
.expectShapeToMatch({ id: ids.box1, x: 1320, y: 845.68 })
|
.expectShapeToMatch({ id: ids.box1, x: 1215, y: 805.9 })
|
||||||
.pointerUp()
|
.pointerUp()
|
||||||
.expectShapeToMatch({ id: ids.box1, x: 1340, y: 857.92 })
|
.expectShapeToMatch({ id: ids.box1, x: 1240, y: 821.2 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('translates multiple shapes', () => {
|
it('translates multiple shapes', () => {
|
||||||
|
|
Loading…
Reference in a new issue