Camera APIs (#1786)
This PR updates camera APIs: - removes animateCamera - adds animation support to setCamera - makes camera commands accept points rather than an x/y - `centerOnPoint` - `pageToScreen` - `screenToPoint` - `pan` - `setCamera` - makes `zoomToBounds` accept a `Box2d` rather than x/y/w/h - removes the `getBoundingClientRects` call from `getPointerInfo` - removes the resize observer from `useScreenBounds`, uses an interval instead when focused A big (unexpected) improvement here is that `getBoundingClientRects` was being called on every pointer move. This is a relatively expensive call (it forces reflow) which could impact interactions. It's now called at most once per second, and we could probably improve on that too if we needed by only updating while in the select state. ### Change Type - [x] `major` — Breaking change ### Test Plan 1. Try the multiple editors example after scrolling / resizing 2. Use the camera commands (zoom in, etc) - [x] Unit Tests ### Release Notes - (editor) improve camera commands
This commit is contained in:
parent
507bba82fd
commit
39dbbca90e
29 changed files with 1625 additions and 640 deletions
|
@ -530,7 +530,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
alignShapes(shapes: TLShape[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
// (undocumented)
|
||||
alignShapes(ids: TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
animateCamera(x: number, y: number, z?: number, opts?: TLAnimationOptions): this;
|
||||
animateShape(partial: null | TLShapePartial | undefined, options?: Partial<{
|
||||
duration: number;
|
||||
ease: (t: number) => number;
|
||||
|
@ -568,7 +567,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
get canUndo(): boolean;
|
||||
// @internal (undocumented)
|
||||
capturedPointerId: null | number;
|
||||
centerOnPoint(x: number, y: number, opts?: TLAnimationOptions): this;
|
||||
centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this;
|
||||
// @internal
|
||||
protected _clickManager: ClickManager;
|
||||
get commonBoundsOfAllShapesOnCurrentPage(): Box2d | undefined;
|
||||
|
@ -841,13 +840,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
packShapes(ids: TLShapeId[], gap: number): this;
|
||||
get pages(): TLPage[];
|
||||
get pageStates(): TLInstancePageState[];
|
||||
pageToScreen(x: number, y: number, z?: number, camera?: VecLike): {
|
||||
pageToScreen(point: VecLike): {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
pan(dx: number, dy: number, opts?: TLAnimationOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], opts?: TLAnimationOptions): this;
|
||||
pan(offset: VecLike, animation?: TLAnimationOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this;
|
||||
popFocusLayer(): this;
|
||||
putContent(content: TLContent, options?: {
|
||||
point?: VecLike;
|
||||
|
@ -882,7 +881,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
reparentShapes(shapes: TLShape[], parentId: TLParentId, insertIndex?: string): this;
|
||||
// (undocumented)
|
||||
reparentShapes(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
||||
resetZoom(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
resetZoom(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
resizeShape(id: TLShapeId, scale: VecLike, options?: {
|
||||
initialBounds?: Box2d;
|
||||
scaleOrigin?: VecLike;
|
||||
|
@ -896,7 +895,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
rotateShapesBy(shapes: TLShape[], delta: number): this;
|
||||
// (undocumented)
|
||||
rotateShapesBy(ids: TLShapeId[], delta: number): this;
|
||||
screenToPage(x: number, y: number, z?: number, camera?: VecLike): {
|
||||
screenToPage(point: VecLike): {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
|
@ -917,7 +916,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
sendToBack(shapes: TLShape[]): this;
|
||||
// (undocumented)
|
||||
sendToBack(ids: TLShapeId[]): this;
|
||||
setCamera(x: number, y: number, z?: number, { stopFollowing }?: TLViewportOptions): this;
|
||||
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
|
||||
// (undocumented)
|
||||
setCroppingId(id: null | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage, opts?: TLViewportOptions): this;
|
||||
|
@ -993,13 +992,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
visitDescendants(parent: TLPage | TLShape, visitor: (id: TLShapeId) => false | void): this;
|
||||
// (undocumented)
|
||||
visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => false | void): this;
|
||||
zoomIn(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
zoomIn(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
get zoomLevel(): number;
|
||||
zoomOut(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: TLAnimationOptions): this;
|
||||
zoomOut(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
zoomToBounds(bounds: Box2d, targetZoom?: number, animation?: TLAnimationOptions): this;
|
||||
zoomToContent(): this;
|
||||
zoomToFit(opts?: TLAnimationOptions): this;
|
||||
zoomToSelection(opts?: TLAnimationOptions): this;
|
||||
zoomToFit(animation?: TLAnimationOptions): this;
|
||||
zoomToSelection(animation?: TLAnimationOptions): this;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1179,7 +1178,7 @@ export function getIndicesBelow(above: string, n: number): string[];
|
|||
export function getIndicesBetween(below: string | undefined, above: string | undefined, n: number): string[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function getPointerInfo(e: PointerEvent | React.PointerEvent, container: HTMLElement): {
|
||||
export function getPointerInfo(e: PointerEvent | React.PointerEvent): {
|
||||
point: {
|
||||
x: number;
|
||||
y: number;
|
||||
|
@ -1625,7 +1624,7 @@ export function refreshPage(): void;
|
|||
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export type RequiredKeys<T, K extends keyof T> = Pick<T, K> & Partial<T>;
|
||||
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function resizeBox(shape: TLBaseBoxShape, info: {
|
||||
|
|
|
@ -590,6 +590,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const hasFocus = container === activeElement || container.contains(activeElement)
|
||||
if ((!isFocused && hasFocus) || (isFocused && !hasFocus)) {
|
||||
this.updateInstanceState({ isFocused: hasFocus })
|
||||
this.updateViewportScreenBounds()
|
||||
}
|
||||
}, 32)
|
||||
|
||||
|
@ -1851,17 +1852,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
private _willSetInitialBounds = true
|
||||
|
||||
/** @internal */
|
||||
private _setCamera(x: number, y: number, z = this.camera.z): this {
|
||||
private _setCamera(point: VecLike): this {
|
||||
const currentCamera = this.camera
|
||||
if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) return this
|
||||
const nextCamera = { ...currentCamera, x, y, z }
|
||||
|
||||
if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.batch(() => {
|
||||
this.store.put([nextCamera])
|
||||
this.store.put([{ ...currentCamera, ...point }]) // include id and meta here
|
||||
|
||||
// Dispatch a new pointer move because the pointer's page will have changed
|
||||
// (its screen position will compute to a new page position given the new camera position)
|
||||
const { currentScreenPoint } = this.inputs
|
||||
|
||||
this.dispatch({
|
||||
|
@ -1888,84 +1890,54 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.setCamera(0, 0)
|
||||
* editor.setCamera(0, 0, 1)
|
||||
* editor.setCamera({ x: 0, y: 0})
|
||||
* editor.setCamera({ x: 0, y: 0, z: 1.5})
|
||||
* editor.setCamera({ x: 0, y: 0, z: 1.5}, { duration: 1000, easing: (t) => t * t })
|
||||
* ```
|
||||
*
|
||||
* @param x - The camera's x position.
|
||||
* @param y - The camera's y position.
|
||||
* @param z - The camera's z position. Defaults to the current zoom.
|
||||
* @param options - Options for the camera change.
|
||||
* @param point - The new camera position.
|
||||
* @param animation - (optional) Options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
setCamera(
|
||||
x: number,
|
||||
y: number,
|
||||
z = this.camera.z,
|
||||
{ stopFollowing = true }: TLViewportOptions = {}
|
||||
): this {
|
||||
setCamera(point: VecLike, animation?: TLAnimationOptions): this {
|
||||
const x = Number.isFinite(point.x) ? point.x : 0
|
||||
const y = Number.isFinite(point.y) ? point.y : 0
|
||||
const z = Number.isFinite(point.z) ? point.z! : this.zoomLevel
|
||||
|
||||
// Stop any camera animations
|
||||
this.stopCameraAnimation()
|
||||
if (stopFollowing && this.instanceState.followingUserId) {
|
||||
|
||||
// Stop following any user
|
||||
if (this.instanceState.followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
x = Number.isNaN(x) ? 0 : x
|
||||
y = Number.isNaN(y) ? 0 : y
|
||||
z = Number.isNaN(z) ? 1 : z
|
||||
this._setCamera(x, y, z)
|
||||
|
||||
if (animation) {
|
||||
const { width, height } = this.viewportScreenBounds
|
||||
return this._animateToViewport(new Box2d(-x, -y, width / z, height / z), animation)
|
||||
} else {
|
||||
this._setCamera({ x, y, z })
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the camera.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.animateCamera(0, 0)
|
||||
* editor.animateCamera(0, 0, 1)
|
||||
* editor.animateCamera(0, 0, 1, { duration: 1000, easing: (t) => t * t })
|
||||
* ```
|
||||
*
|
||||
* @param x - The camera's x position.
|
||||
* @param y - The camera's y position.
|
||||
* @param z - The camera's z position. Defaults to the current zoom.
|
||||
* @param opts - Options for the animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
animateCamera(
|
||||
x: number,
|
||||
y: number,
|
||||
z = this.camera.z,
|
||||
opts: TLAnimationOptions = DEFAULT_ANIMATION_OPTIONS
|
||||
): this {
|
||||
x = Number.isNaN(x) ? 0 : x
|
||||
y = Number.isNaN(y) ? 0 : y
|
||||
z = Number.isNaN(z) ? 1 : z
|
||||
const { width, height } = this.viewportScreenBounds
|
||||
const w = width / z
|
||||
const h = height / z
|
||||
|
||||
const targetViewport = new Box2d(-x, -y, w, h)
|
||||
|
||||
return this._animateToViewport(targetViewport, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Center the camera on a point (in page space).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.centerOnPoint(100, 100)
|
||||
* editor.centerOnPoint({ x: 100, y: 100 })
|
||||
* editor.centerOnPoint({ x: 100, y: 100 }, { duration: 200 })
|
||||
* ```
|
||||
*
|
||||
* @param x - The x position of the point.
|
||||
* @param y - The y position of the point.
|
||||
* @param opts - The options for an animation.
|
||||
* @param point - The point in page space to center on.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
centerOnPoint(x: number, y: number, opts?: TLAnimationOptions): this {
|
||||
centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const {
|
||||
|
@ -1973,31 +1945,28 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
camera,
|
||||
} = this
|
||||
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(-(x - pw / 2), -(y - ph / 2), camera.z, opts)
|
||||
} else {
|
||||
this.setCamera(-(x - pw / 2), -(y - ph / 2), camera.z)
|
||||
}
|
||||
this.setCamera({ x: -(point.x - pw / 2), y: -(point.y - ph / 2), z: camera.z }, animation)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the camera to the nearest content.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.zoomToContent()
|
||||
* editor.zoomToContent({ duration: 200 })
|
||||
* ```
|
||||
*
|
||||
* @param opts - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomToContent() {
|
||||
const bounds = this.selectionPageBounds ?? this.commonBoundsOfAllShapesOnCurrentPage
|
||||
|
||||
if (bounds) {
|
||||
this.zoomToBounds(
|
||||
bounds.minX,
|
||||
bounds.minY,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
Math.min(1, this.zoomLevel),
|
||||
{ duration: 220 }
|
||||
)
|
||||
this.zoomToBounds(bounds, Math.min(1, this.zoomLevel), { duration: 220 })
|
||||
}
|
||||
|
||||
return this
|
||||
|
@ -2009,25 +1978,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @example
|
||||
* ```ts
|
||||
* editor.zoomToFit()
|
||||
* editor.zoomToFit({ duration: 200 })
|
||||
* ```
|
||||
*
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomToFit(opts?: TLAnimationOptions): this {
|
||||
zoomToFit(animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const ids = [...this.shapeIdsOnCurrentPage]
|
||||
if (ids.length <= 0) return this
|
||||
|
||||
const pageBounds = Box2d.Common(compact(ids.map((id) => this.getPageBounds(id))))
|
||||
this.zoomToBounds(
|
||||
pageBounds.minX,
|
||||
pageBounds.minY,
|
||||
pageBounds.width,
|
||||
pageBounds.height,
|
||||
undefined,
|
||||
opts
|
||||
)
|
||||
this.zoomToBounds(pageBounds, undefined, animation)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2037,22 +2002,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @example
|
||||
* ```ts
|
||||
* editor.resetZoom()
|
||||
* editor.resetZoom(editor.viewportScreenCenter)
|
||||
* editor.resetZoom(editor.viewportScreenCenter, { duration: 200 })
|
||||
* ```
|
||||
*
|
||||
* @param opts - The options for an animation.
|
||||
* @param point - (optional) The screen point to zoom out on. Defaults to the viewport screen center.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
resetZoom(point = this.viewportScreenCenter, opts?: TLAnimationOptions): this {
|
||||
resetZoom(point = this.viewportScreenCenter, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.camera
|
||||
const { x, y } = point
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1, opts)
|
||||
} else {
|
||||
this.setCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1)
|
||||
}
|
||||
this.setCamera(
|
||||
{ x: cx + (x / 1 - x) - (x / cz - x), y: cy + (y / 1 - y) - (y / cz - y), z: 1 },
|
||||
animation
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -2067,11 +2034,26 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* editor.zoomIn(editor.inputs.currentScreenPoint, { duration: 120 })
|
||||
* ```
|
||||
*
|
||||
* @param opts - The options for an animation.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomIn(point = this.viewportScreenCenter, opts?: TLAnimationOptions): this {
|
||||
|
||||
/**
|
||||
* Zoom the camera in.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.zoomIn()
|
||||
* editor.zoomIn(editor.viewportScreenCenter, { duration: 120 })
|
||||
* editor.zoomIn(editor.inputs.currentScreenPoint, { duration: 120 })
|
||||
* ```
|
||||
*
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomIn(point = this.viewportScreenCenter, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.camera
|
||||
|
@ -2087,16 +2069,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
|
||||
const { x, y } = point
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(
|
||||
cx + (x / zoom - x) - (x / cz - x),
|
||||
cy + (y / zoom - y) - (y / cz - y),
|
||||
zoom,
|
||||
opts
|
||||
)
|
||||
} else {
|
||||
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
|
||||
}
|
||||
this.setCamera(
|
||||
{ x: cx + (x / zoom - x) - (x / cz - x), y: cy + (y / zoom - y) - (y / cz - y), z: zoom },
|
||||
animation
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -2111,11 +2087,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* editor.zoomOut(editor.inputs.currentScreenPoint, { duration: 120 })
|
||||
* ```
|
||||
*
|
||||
* @param opts - The options for an animation.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomOut(point = this.viewportScreenCenter, opts?: TLAnimationOptions): this {
|
||||
zoomOut(point = this.viewportScreenCenter, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.camera
|
||||
|
@ -2132,16 +2108,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const { x, y } = point
|
||||
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(
|
||||
cx + (x / zoom - x) - (x / cz - x),
|
||||
cy + (y / zoom - y) - (y / cz - y),
|
||||
zoom,
|
||||
opts
|
||||
)
|
||||
} else {
|
||||
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
|
||||
}
|
||||
this.setCamera(
|
||||
{
|
||||
x: cx + (x / zoom - x) - (x / cz - x),
|
||||
y: cy + (y / zoom - y) - (y / cz - y),
|
||||
z: zoom,
|
||||
},
|
||||
animation
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -2154,26 +2128,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* editor.zoomToSelection()
|
||||
* ```
|
||||
*
|
||||
* @param opts - The options for an animation.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomToSelection(opts?: TLAnimationOptions): this {
|
||||
zoomToSelection(animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const ids = this.selectedShapeIds
|
||||
if (ids.length <= 0) return this
|
||||
|
||||
const selectedBounds = Box2d.Common(compact(ids.map((id) => this.getPageBounds(id))))
|
||||
const selectionBounds = Box2d.Common(compact(ids.map((id) => this.getPageBounds(id))))
|
||||
|
||||
this.zoomToBounds(
|
||||
selectedBounds.minX,
|
||||
selectedBounds.minY,
|
||||
selectedBounds.width,
|
||||
selectedBounds.height,
|
||||
Math.max(1, this.camera.z),
|
||||
opts
|
||||
)
|
||||
this.zoomToBounds(selectionBounds, Math.max(1, this.camera.z), animation)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -2182,27 +2149,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.
|
||||
*
|
||||
* @param ids - The ids of the shapes to pan and zoom into view.
|
||||
* @param opts - The options for an animation.
|
||||
* @param animation - The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
panZoomIntoView(ids: TLShapeId[], opts?: TLAnimationOptions): this {
|
||||
panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
if (ids.length <= 0) return this
|
||||
const selectedBounds = Box2d.Common(compact(ids.map((id) => this.getPageBounds(id))))
|
||||
const selectionBounds = Box2d.Common(compact(ids.map((id) => this.getPageBounds(id))))
|
||||
|
||||
const { viewportPageBounds } = this
|
||||
|
||||
if (viewportPageBounds.h < selectedBounds.h || viewportPageBounds.w < selectedBounds.w) {
|
||||
this.zoomToBounds(
|
||||
selectedBounds.minX,
|
||||
selectedBounds.minY,
|
||||
selectedBounds.width,
|
||||
selectedBounds.height,
|
||||
this.camera.z,
|
||||
opts
|
||||
)
|
||||
if (viewportPageBounds.h < selectionBounds.h || viewportPageBounds.w < selectionBounds.w) {
|
||||
this.zoomToBounds(selectionBounds, this.camera.z, animation)
|
||||
|
||||
return this
|
||||
} else {
|
||||
|
@ -2210,33 +2170,28 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
let offsetX = 0
|
||||
let offsetY = 0
|
||||
if (insetViewport.maxY < selectedBounds.maxY) {
|
||||
if (insetViewport.maxY < selectionBounds.maxY) {
|
||||
// off bottom
|
||||
offsetY = insetViewport.maxY - selectedBounds.maxY
|
||||
} else if (insetViewport.minY > selectedBounds.minY) {
|
||||
offsetY = insetViewport.maxY - selectionBounds.maxY
|
||||
} else if (insetViewport.minY > selectionBounds.minY) {
|
||||
// off top
|
||||
offsetY = insetViewport.minY - selectedBounds.minY
|
||||
offsetY = insetViewport.minY - selectionBounds.minY
|
||||
} else {
|
||||
// inside y-bounds
|
||||
}
|
||||
|
||||
if (insetViewport.maxX < selectedBounds.maxX) {
|
||||
if (insetViewport.maxX < selectionBounds.maxX) {
|
||||
// off right
|
||||
offsetX = insetViewport.maxX - selectedBounds.maxX
|
||||
} else if (insetViewport.minX > selectedBounds.minX) {
|
||||
offsetX = insetViewport.maxX - selectionBounds.maxX
|
||||
} else if (insetViewport.minX > selectionBounds.minX) {
|
||||
// off left
|
||||
offsetX = insetViewport.minX - selectedBounds.minX
|
||||
offsetX = insetViewport.minX - selectionBounds.minX
|
||||
} else {
|
||||
// inside x-bounds
|
||||
}
|
||||
|
||||
const { camera } = this
|
||||
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(camera.x + offsetX, camera.y + offsetY, camera.z, opts)
|
||||
} else {
|
||||
this.setCamera(camera.x + offsetX, camera.y + offsetY, camera.z)
|
||||
}
|
||||
this.setCamera({ x: camera.x + offsetX, y: camera.y + offsetY, z: camera.z }, animation)
|
||||
}
|
||||
|
||||
return this
|
||||
|
@ -2247,25 +2202,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.zoomToBounds(0, 0, 100, 100)
|
||||
* editor.zoomToBounds(myBounds)
|
||||
* editor.zoomToBounds(myBounds, 1)
|
||||
* editor.zoomToBounds(myBounds, 1, { duration: 100 })
|
||||
* ```
|
||||
*
|
||||
* @param x - The bounding box's x position.
|
||||
* @param y - The bounding box's y position.
|
||||
* @param width - The bounding box's width.
|
||||
* @param height - The bounding box's height.
|
||||
* @param bounds - The bounding box.
|
||||
* @param targetZoom - The desired zoom level. Defaults to 0.1.
|
||||
* @param animation - (optional) The options for an animation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
zoomToBounds(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
targetZoom?: number,
|
||||
opts?: TLAnimationOptions
|
||||
): this {
|
||||
zoomToBounds(bounds: Box2d, targetZoom?: number, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const { viewportScreenBounds } = this
|
||||
|
@ -2274,8 +2222,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
let zoom = clamp(
|
||||
Math.min(
|
||||
(viewportScreenBounds.width - inset) / width,
|
||||
(viewportScreenBounds.height - inset) / height
|
||||
(viewportScreenBounds.width - inset) / bounds.width,
|
||||
(viewportScreenBounds.height - inset) / bounds.height
|
||||
),
|
||||
MIN_ZOOM,
|
||||
MAX_ZOOM
|
||||
|
@ -2285,20 +2233,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
zoom = Math.min(targetZoom, zoom)
|
||||
}
|
||||
|
||||
if (opts?.duration) {
|
||||
this.animateCamera(
|
||||
-x + (viewportScreenBounds.width - width * zoom) / 2 / zoom,
|
||||
-y + (viewportScreenBounds.height - height * zoom) / 2 / zoom,
|
||||
zoom,
|
||||
opts
|
||||
)
|
||||
} else {
|
||||
this.setCamera(
|
||||
-x + (viewportScreenBounds.width - width * zoom) / 2 / zoom,
|
||||
-y + (viewportScreenBounds.height - height * zoom) / 2 / zoom,
|
||||
zoom
|
||||
)
|
||||
}
|
||||
this.setCamera(
|
||||
{
|
||||
x: -bounds.minX + (viewportScreenBounds.width - bounds.width * zoom) / 2 / zoom,
|
||||
y: -bounds.minY + (viewportScreenBounds.height - bounds.height * zoom) / 2 / zoom,
|
||||
z: zoom,
|
||||
},
|
||||
animation
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -2308,27 +2250,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.pan(100, 100)
|
||||
* editor.pan(100, 100, { duration: 1000 })
|
||||
* editor.pan({ x: 100, y: 100 })
|
||||
* editor.pan({ x: 100, y: 100 }, { duration: 1000 })
|
||||
* ```
|
||||
*
|
||||
* @param dx - The amount to pan on the x axis.
|
||||
* @param dy - The amount to pan on the y axis.
|
||||
* @param opts - The animation options
|
||||
* @param offset - The offset in page space.
|
||||
* @param animation - (optional) The animation options.
|
||||
*/
|
||||
pan(dx: number, dy: number, opts?: TLAnimationOptions): this {
|
||||
pan(offset: VecLike, animation?: TLAnimationOptions): this {
|
||||
if (!this.instanceState.canMoveCamera) return this
|
||||
|
||||
const { camera } = this
|
||||
const { x: cx, y: cy, z: cz } = camera
|
||||
const d = new Vec2d(dx, dy).div(cz)
|
||||
|
||||
if (opts?.duration ?? 0 > 0) {
|
||||
return this.animateCamera(cx + d.x, cy + d.y, cz, opts)
|
||||
} else {
|
||||
this.setCamera(cx + d.x, cy + d.y, cz)
|
||||
}
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.camera
|
||||
this.setCamera({ x: cx + offset.x / cz, y: cy + offset.y / cz, z: cz }, animation)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2369,11 +2301,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const { elapsed, easing, duration, start, end } = this._viewportAnimation
|
||||
|
||||
if (elapsed > duration) {
|
||||
const z = this.viewportScreenBounds.width / end.width
|
||||
const x = -end.x
|
||||
const y = -end.y
|
||||
|
||||
this._setCamera(x, y, z)
|
||||
this._setCamera({ x: -end.x, y: -end.y, z: this.viewportScreenBounds.width / end.width })
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
@ -2384,15 +2312,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const left = start.minX + (end.minX - start.minX) * t
|
||||
const top = start.minY + (end.minY - start.minY) * t
|
||||
const right = start.maxX + (end.maxX - start.maxX) * t
|
||||
const bottom = start.maxY + (end.maxY - start.maxY) * t
|
||||
|
||||
const easedViewport = new Box2d(left, top, right - left, bottom - top)
|
||||
|
||||
const z = this.viewportScreenBounds.width / easedViewport.width
|
||||
const x = -easedViewport.x
|
||||
const y = -easedViewport.y
|
||||
|
||||
this._setCamera(x, y, z)
|
||||
this._setCamera({ x: -left, y: -top, z: this.viewportScreenBounds.width / (right - left) })
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -2411,11 +2332,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
if (duration === 0 || animationSpeed === 0) {
|
||||
// If we have no animation, then skip the animation and just set the camera
|
||||
return this._setCamera(
|
||||
-targetViewportPage.x,
|
||||
-targetViewportPage.y,
|
||||
this.viewportScreenBounds.width / targetViewportPage.width
|
||||
)
|
||||
return this._setCamera({
|
||||
x: -targetViewportPage.x,
|
||||
y: -targetViewportPage.y,
|
||||
z: this.viewportScreenBounds.width / targetViewportPage.width,
|
||||
})
|
||||
}
|
||||
|
||||
// Set our viewport animation
|
||||
|
@ -2424,7 +2345,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
duration: duration / animationSpeed,
|
||||
easing,
|
||||
start: viewportPageBounds.clone(),
|
||||
end: targetViewportPage,
|
||||
end: targetViewportPage.clone(),
|
||||
}
|
||||
|
||||
// On each tick, animate the viewport
|
||||
|
@ -2474,7 +2395,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (currentSpeed < speedThreshold) {
|
||||
cancel()
|
||||
} else {
|
||||
this._setCamera(cx + movementVec.x, cy + movementVec.y, cz)
|
||||
this._setCamera({ x: cx + movementVec.x, y: cy + movementVec.y, z: cz })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2518,9 +2439,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// Only animate the camera if the user is on the same page as us
|
||||
const options = isOnSamePage ? { duration: 500 } : undefined
|
||||
|
||||
const position = presence.cursor
|
||||
|
||||
this.centerOnPoint(position.x, position.y, options)
|
||||
this.centerOnPoint(presence.cursor, options)
|
||||
|
||||
// Highlight the user's cursor
|
||||
const { highlightedUserIds } = this.instanceState
|
||||
|
@ -2575,6 +2494,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// Viewport
|
||||
|
||||
/** @internal */
|
||||
private _willSetInitialBounds = true
|
||||
|
||||
/**
|
||||
* Update the viewport. The viewport will measure the size and screen position of its container
|
||||
* element. This should be done whenever the container's position on the screen changes.
|
||||
|
@ -2594,11 +2516,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
if (!container) return this
|
||||
const rect = container.getBoundingClientRect()
|
||||
const screenBounds = new Box2d(0, 0, Math.max(rect.width, 1), Math.max(rect.height, 1))
|
||||
|
||||
const screenBounds = new Box2d(
|
||||
rect.left || rect.x,
|
||||
rect.top || rect.y,
|
||||
Math.max(rect.width, 1),
|
||||
Math.max(rect.height, 1)
|
||||
)
|
||||
const boundsAreEqual = screenBounds.equals(this.viewportScreenBounds)
|
||||
|
||||
// Get the current value
|
||||
const { _willSetInitialBounds } = this
|
||||
|
||||
if (boundsAreEqual) {
|
||||
|
@ -2609,21 +2534,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this._willSetInitialBounds = false
|
||||
this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true)
|
||||
} else {
|
||||
const { zoomLevel } = this
|
||||
if (center) {
|
||||
if (center && !this.instanceState.followingUserId) {
|
||||
// Get the page center before the change, make the change, and restore it
|
||||
const before = this.viewportPageCenter
|
||||
this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true)
|
||||
const after = this.viewportPageCenter
|
||||
if (!this.instanceState.followingUserId) {
|
||||
this.pan((after.x - before.x) * zoomLevel, (after.y - before.y) * zoomLevel)
|
||||
}
|
||||
this.centerOnPoint(before)
|
||||
} else {
|
||||
const before = this.screenToPage(0, 0)
|
||||
// Otherwise,
|
||||
this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true)
|
||||
const after = this.screenToPage(0, 0)
|
||||
if (!this.instanceState.followingUserId) {
|
||||
this.pan((after.x - before.x) * zoomLevel, (after.y - before.y) * zoomLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2665,10 +2583,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
@computed get viewportPageBounds() {
|
||||
const { x, y, w, h } = this.viewportScreenBounds
|
||||
const tl = this.screenToPage(x, y)
|
||||
const br = this.screenToPage(x + w, y + h)
|
||||
return new Box2d(tl.x, tl.y, br.x - tl.x, br.y - tl.y)
|
||||
const { w, h } = this.viewportScreenBounds
|
||||
const { x: cx, y: cy, z: cz } = this.camera
|
||||
return new Box2d(-cx, -cy, w / cz, h / cz)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2685,45 +2602,43 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.screenToPage(100, 100)
|
||||
* editor.screenToPage({ x: 100, y: 100 })
|
||||
* ```
|
||||
*
|
||||
* @param x - The x coordinate of the point in screen space.
|
||||
* @param y - The y coordinate of the point in screen space.
|
||||
* @param camera - The camera to use. Defaults to the current camera.
|
||||
* @param point - The point in screen space.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
screenToPage(x: number, y: number, z = 0.5, camera: VecLike = this.camera) {
|
||||
screenToPage(point: VecLike) {
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x: cx, y: cy, z: cz = 1 } = camera
|
||||
const { x: cx, y: cy, z: cz = 1 } = this.camera
|
||||
return {
|
||||
x: (x - screenBounds.x) / cz - cx,
|
||||
y: (y - screenBounds.y) / cz - cy,
|
||||
z,
|
||||
x: (point.x - screenBounds.x - cx) / cz,
|
||||
y: (point.y - screenBounds.y - cy) / cz,
|
||||
z: point.z ?? 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a point in page space to a point in screen space.
|
||||
* Convert a point in page space to a point in current screen space.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.pageToScreen(100, 100)
|
||||
* editor.pageToScreen({ x: 100, y: 100 })
|
||||
* ```
|
||||
*
|
||||
* @param x - The x coordinate of the point in screen space.
|
||||
* @param y - The y coordinate of the point in screen space.
|
||||
* @param camera - The camera to use. Defaults to the current camera.
|
||||
* @param point - The point in screen space.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
pageToScreen(x: number, y: number, z = 0.5, camera: VecLike = this.camera) {
|
||||
const { x: cx, y: cy, z: cz = 1 } = camera
|
||||
pageToScreen(point: VecLike) {
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x: cx, y: cy, z: cz = 1 } = this.camera
|
||||
|
||||
return {
|
||||
x: x + cx * cz,
|
||||
y: y + cy * cz,
|
||||
z,
|
||||
x: point.x * cz + cx + screenBounds.x,
|
||||
y: point.y * cz + cy + screenBounds.y,
|
||||
z: point.z ?? 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2781,7 +2696,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const isOnSamePage = leaderPresence.currentPageId === this.currentPageId
|
||||
const chaseProportion = isOnSamePage ? FOLLOW_CHASE_PROPORTION : 1
|
||||
if (!isOnSamePage) {
|
||||
this.stopFollowingUser()
|
||||
this.setCurrentPage(leaderPresence.currentPageId, { stopFollowing: false })
|
||||
this.startFollowingUser(userId)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the bounds of the follower (me) and the leader (them)
|
||||
|
@ -2838,12 +2756,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// Update the camera!
|
||||
isCaughtUp = false
|
||||
this.stopCameraAnimation()
|
||||
this.setCamera(
|
||||
-(targetCenter.x - targetWidth / 2),
|
||||
-(targetCenter.y - targetHeight / 2),
|
||||
targetZoom,
|
||||
{ stopFollowing: false }
|
||||
)
|
||||
this._setCamera({
|
||||
x: -(targetCenter.x - targetWidth / 2),
|
||||
y: -(targetCenter.y - targetHeight / 2),
|
||||
z: targetZoom,
|
||||
})
|
||||
}
|
||||
|
||||
this.once('stop-following', cancel)
|
||||
|
@ -3483,7 +3400,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this.batch(() => {
|
||||
this.createPage(page.name + ' Copy', createId, page.index)
|
||||
this.setCurrentPage(createId)
|
||||
this.setCamera(camera.x, camera.y, camera.z)
|
||||
this.setCamera(camera)
|
||||
|
||||
// will change page automatically
|
||||
if (content) {
|
||||
|
@ -5186,7 +5103,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// new shapes.
|
||||
const { viewportPageBounds, selectionPageBounds: selectionPageBounds } = this
|
||||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
this.centerOnPoint(selectionPageBounds.center.x, selectionPageBounds.center.y, {
|
||||
this.centerOnPoint(selectionPageBounds.center, {
|
||||
duration: ANIMATION_MEDIUM_MS,
|
||||
})
|
||||
}
|
||||
|
@ -5255,11 +5172,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// Force the new page's camera to be at the same zoom level as the
|
||||
// "from" page's camera, then center the "to" page's camera on the
|
||||
// pasted shapes
|
||||
const {
|
||||
center: { x, y },
|
||||
} = this.selectionBounds!
|
||||
this.setCamera(this.camera.x, this.camera.y, fromPageZ)
|
||||
this.centerOnPoint(x, y)
|
||||
this.setCamera({ ...this.camera, z: fromPageZ })
|
||||
this.centerOnPoint(this.selectionBounds!.center)
|
||||
})
|
||||
|
||||
return this
|
||||
|
@ -5294,16 +5208,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (allUnlocked) {
|
||||
this.updateShapes(shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true })))
|
||||
this.setSelectedShapeIds([])
|
||||
} else if (allLocked) {
|
||||
this.updateShapes(
|
||||
shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
|
||||
)
|
||||
} else {
|
||||
this.updateShapes(shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true })))
|
||||
}
|
||||
this.batch(() => {
|
||||
if (allUnlocked) {
|
||||
this.updateShapes(
|
||||
shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
||||
)
|
||||
this.setSelectedShapeIds([])
|
||||
} else if (allLocked) {
|
||||
this.updateShapes(
|
||||
shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
|
||||
)
|
||||
} else {
|
||||
this.updateShapes(
|
||||
shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -8487,11 +8407,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z))
|
||||
|
||||
this.setCamera(
|
||||
cx + dx / cz - x / cz + x / zoom,
|
||||
cy + dy / cz - y / cz + y / zoom,
|
||||
zoom
|
||||
)
|
||||
this.setCamera({
|
||||
x: cx + dx / cz - x / cz + x / zoom,
|
||||
y: cy + dy / cz - y / cz + y / zoom,
|
||||
z: zoom,
|
||||
})
|
||||
|
||||
return // Stop here!
|
||||
}
|
||||
|
@ -8521,10 +8441,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
if (zoom !== undefined) {
|
||||
const { x, y } = this.viewportScreenCenter
|
||||
this.animateCamera(
|
||||
cx + (x / zoom - x) - (x / cz - x),
|
||||
cy + (y / zoom - y) - (y / cz - y),
|
||||
zoom,
|
||||
this.setCamera(
|
||||
{
|
||||
x: cx + (x / zoom - x) - (x / cz - x),
|
||||
y: cy + (y / zoom - y) - (y / cz - y),
|
||||
z: zoom,
|
||||
},
|
||||
{ duration: 100 }
|
||||
)
|
||||
}
|
||||
|
@ -8558,11 +8480,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz))
|
||||
|
||||
this.setCamera(
|
||||
cx + (x / zoom - x) - (x / cz - x),
|
||||
cy + (y / zoom - y) - (y / cz - y),
|
||||
zoom
|
||||
)
|
||||
this.setCamera({
|
||||
x: cx + (x / zoom - x) - (x / cz - x),
|
||||
y: cy + (y / zoom - y) - (y / cz - y),
|
||||
z: zoom,
|
||||
})
|
||||
|
||||
// We want to return here because none of the states in our
|
||||
// statechart should respond to this event (a camera zoom)
|
||||
|
@ -8571,7 +8493,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// Update the camera here, which will dispatch a pointer move...
|
||||
// this will also update the pointer position, etc
|
||||
this.pan(info.delta.x, info.delta.y)
|
||||
this.pan(info.delta)
|
||||
|
||||
if (
|
||||
!inputs.isDragging &&
|
||||
|
@ -8657,8 +8579,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (this.inputs.isPanning && this.inputs.isPointing) {
|
||||
// Handle panning
|
||||
const { currentScreenPoint, previousScreenPoint } = this.inputs
|
||||
const delta = Vec2d.Sub(currentScreenPoint, previousScreenPoint)
|
||||
this.pan(delta.x, delta.y)
|
||||
this.pan(Vec2d.Sub(currentScreenPoint, previousScreenPoint))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
/** @public */
|
||||
export type RequiredKeys<T, K extends keyof T> = Pick<T, K> & Partial<T>
|
||||
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
|
||||
/** @public */
|
||||
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
|
|
@ -21,7 +21,7 @@ export function useCanvasEvents() {
|
|||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_down',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ export function useCanvasEvents() {
|
|||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ export function useCanvasEvents() {
|
|||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_up',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -84,12 +84,10 @@ export function useCanvasEvents() {
|
|||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
|
||||
const rect = editor.getContainer().getBoundingClientRect()
|
||||
|
||||
await editor.putExternalContent({
|
||||
type: 'files',
|
||||
files,
|
||||
point: editor.screenToPage(e.clientX - rect.x, e.clientY - rect.y),
|
||||
point: editor.screenToPage({ x: e.clientX, y: e.clientY }),
|
||||
ignoreParent: false,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) {
|
|||
handle,
|
||||
shape,
|
||||
name: 'pointer_down',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) {
|
|||
handle,
|
||||
shape,
|
||||
name: 'pointer_move',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) {
|
|||
handle,
|
||||
shape,
|
||||
name: 'pointer_up',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
import throttle from 'lodash.throttle'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { useContainer } from './useContainer'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
export function useScreenBounds() {
|
||||
const editor = useEditor()
|
||||
const container = useContainer()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateBounds = throttle(
|
||||
() => {
|
||||
editor.updateViewportScreenBounds()
|
||||
if (editor.instanceState.isFocused) {
|
||||
editor.updateViewportScreenBounds()
|
||||
}
|
||||
},
|
||||
200,
|
||||
{ trailing: true }
|
||||
{
|
||||
trailing: true,
|
||||
}
|
||||
)
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (entries[0].contentRect) {
|
||||
updateBounds()
|
||||
}
|
||||
})
|
||||
|
||||
if (container) {
|
||||
resizeObserver.observe(container)
|
||||
}
|
||||
|
||||
// Rather than running getClientRects on every frame, we'll
|
||||
// run it once a second or when the window resizes / scrolls.
|
||||
updateBounds()
|
||||
const interval = setInterval(updateBounds, 1000)
|
||||
window.addEventListener('resize', updateBounds)
|
||||
window.addEventListener('scroll', updateBounds)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
clearInterval(interval)
|
||||
window.removeEventListener('resize', updateBounds)
|
||||
window.removeEventListener('scroll', updateBounds)
|
||||
}
|
||||
}, [editor, container])
|
||||
})
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
|||
type: 'pointer',
|
||||
target: 'selection',
|
||||
handle,
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
|||
type: 'pointer',
|
||||
target: 'selection',
|
||||
handle,
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
|||
type: 'pointer',
|
||||
target: 'selection',
|
||||
handle,
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
/** @public */
|
||||
export function getPointerInfo(e: React.PointerEvent | PointerEvent, container: HTMLElement) {
|
||||
export function getPointerInfo(e: React.PointerEvent | PointerEvent) {
|
||||
;(e as any).isKilled = true
|
||||
|
||||
const { top, left } = container.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
point: {
|
||||
x: e.clientX - left,
|
||||
y: e.clientY - top,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
z: e.pressure,
|
||||
},
|
||||
shiftKey: e.shiftKey,
|
||||
|
|
|
@ -30,7 +30,7 @@ export const FrameHeading = function FrameHeading({
|
|||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const event = getPointerInfo(e, editor.getContainer())
|
||||
const event = getPointerInfo(e)
|
||||
editor.dispatch({
|
||||
type: 'pointer',
|
||||
name: 'pointer_down',
|
||||
|
|
|
@ -225,7 +225,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
const handleContentPointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
editor.dispatch({
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
type: 'pointer',
|
||||
name: 'pointer_down',
|
||||
target: 'shape',
|
||||
|
|
|
@ -29,7 +29,7 @@ export class Dragging extends StateNode {
|
|||
const delta = Vec2d.Sub(currentScreenPoint, previousScreenPoint)
|
||||
|
||||
if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) {
|
||||
this.editor.pan(delta.x, delta.y)
|
||||
this.editor.pan(delta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,14 +54,7 @@ export class ZoomBrushing extends StateNode {
|
|||
}
|
||||
} else {
|
||||
const zoomLevel = this.editor.inputs.altKey ? this.editor.zoomLevel / 2 : undefined
|
||||
this.editor.zoomToBounds(
|
||||
zoomBrush.x,
|
||||
zoomBrush.y,
|
||||
zoomBrush.width,
|
||||
zoomBrush.height,
|
||||
zoomLevel,
|
||||
{ duration: 220 }
|
||||
)
|
||||
this.editor.zoomToBounds(zoomBrush, zoomLevel, { duration: 220 })
|
||||
}
|
||||
|
||||
this.parent.transition('idle', this.info)
|
||||
|
|
|
@ -56,14 +56,14 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!editor.shapeIdsOnCurrentPage.size) return
|
||||
|
||||
const { x, y } = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
|
||||
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
|
||||
|
||||
minimap.originPagePoint.setTo(clampedPoint)
|
||||
minimap.originPageCenter.setTo(editor.viewportPageBounds.center)
|
||||
|
||||
editor.centerOnPoint(x, y, { duration: ANIMATION_MEDIUM_MS })
|
||||
editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
[editor, minimap]
|
||||
)
|
||||
|
@ -77,7 +77,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
|
||||
minimap.isInViewport = false
|
||||
|
||||
const { x, y } = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
|
||||
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
|
||||
|
||||
|
@ -89,7 +89,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
||||
|
||||
if (!minimap.isInViewport) {
|
||||
editor.centerOnPoint(x, y, { duration: ANIMATION_MEDIUM_MS })
|
||||
editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS })
|
||||
}
|
||||
},
|
||||
[editor, minimap]
|
||||
|
@ -98,32 +98,27 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
const onPointerMove = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (rPointing.current) {
|
||||
const { x, y } = minimap.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
e.shiftKey,
|
||||
true
|
||||
)
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, e.shiftKey, true)
|
||||
|
||||
if (minimap.isInViewport) {
|
||||
const delta = Vec2d.Sub({ x, y }, minimap.originPagePoint)
|
||||
const delta = point.clone().sub(minimap.originPagePoint).add(minimap.originPageCenter)
|
||||
const center = Vec2d.Add(minimap.originPageCenter, delta)
|
||||
editor.centerOnPoint(center.x, center.y)
|
||||
editor.centerOnPoint(center)
|
||||
return
|
||||
}
|
||||
|
||||
editor.centerOnPoint(x, y)
|
||||
editor.centerOnPoint(point)
|
||||
}
|
||||
|
||||
const pagePoint = minimap.getPagePoint(e.clientX, e.clientY)
|
||||
|
||||
const screenPoint = editor.pageToScreen(pagePoint.x, pagePoint.y)
|
||||
const screenPoint = editor.pageToScreen(pagePoint)
|
||||
|
||||
const info: TLPointerEventInfo = {
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
...getPointerInfo(e, editor.getContainer()),
|
||||
...getPointerInfo(e),
|
||||
point: screenPoint,
|
||||
isPen: editor.instanceState.isPenMode,
|
||||
}
|
||||
|
|
|
@ -104,11 +104,11 @@ export class MinimapManager {
|
|||
|
||||
const { x: screenX, y: screenY } = this.getScreenPoint(x, y)
|
||||
|
||||
return {
|
||||
x: canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
|
||||
y: canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
|
||||
z: 1,
|
||||
}
|
||||
return new Vec2d(
|
||||
canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
|
||||
canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
minimapScreenPointToPagePoint = (
|
||||
|
@ -168,7 +168,7 @@ export class MinimapManager {
|
|||
}
|
||||
}
|
||||
|
||||
return { x: px, y: py }
|
||||
return new Vec2d(px, py)
|
||||
}
|
||||
|
||||
render = () => {
|
||||
|
|
|
@ -593,7 +593,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
|
||||
const bounds = editor.commonBoundsOfAllShapesOnCurrentPage
|
||||
if (bounds) {
|
||||
editor.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1)
|
||||
editor.zoomToBounds(bounds, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -293,7 +293,7 @@ export async function parseAndLoadDocument(
|
|||
|
||||
const bounds = editor.commonBoundsOfAllShapesOnCurrentPage
|
||||
if (bounds) {
|
||||
editor.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1)
|
||||
editor.zoomToBounds(bounds, 1)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
it('Correctly zooms in when clicking', () => {
|
||||
editor.keyDown('z')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.click()
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
@ -52,7 +52,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
editor.keyDown('z')
|
||||
editor.keyDown('Alt')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.click()
|
||||
jest.advanceTimersByTime(300)
|
||||
|
@ -109,7 +109,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
|
||||
it('When the dragged area is small it zooms in instead of zooming to the area', () => {
|
||||
const originalCenter = { x: 540, y: 360 }
|
||||
const originalPageBounds = { x: 0, y: 0, w: 1080, h: 720 }
|
||||
const originalPageBounds = { x: -0, y: -0, w: 1080, h: 720 }
|
||||
const change = 6
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject(originalPageBounds)
|
||||
|
@ -143,7 +143,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
const newBoundsY = 200
|
||||
editor.expectToBeIn('select.idle')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.keyDown('z')
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
@ -179,7 +179,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
editor.expectToBeIn('select.idle')
|
||||
const originalZoomLevel = 1
|
||||
expect(editor.zoomLevel).toBe(originalZoomLevel)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.keyDown('z')
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
|
|
@ -7,6 +7,15 @@ beforeEach(() => {
|
|||
})
|
||||
|
||||
it('centers on the point', () => {
|
||||
editor.centerOnPoint(400, 400)
|
||||
editor.centerOnPoint({ x: 400, y: 400 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 400, y: 400 })
|
||||
})
|
||||
|
||||
it('centers on the point with animation', () => {
|
||||
editor.centerOnPoint({ x: 400, y: 400 }, { duration: 200 })
|
||||
expect(editor.viewportPageCenter).not.toMatchObject({ x: 400, y: 400 })
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.viewportPageCenter).not.toMatchObject({ x: 400, y: 400 })
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 400, y: 400 })
|
||||
})
|
||||
|
|
|
@ -75,7 +75,11 @@ describe('When copying and pasting', () => {
|
|||
|
||||
const testOffsetX = 100
|
||||
const testOffsetY = 100
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
@ -116,7 +120,11 @@ describe('When copying and pasting', () => {
|
|||
|
||||
const testOffsetX = 1800
|
||||
const testOffsetY = 0
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
@ -154,7 +162,11 @@ describe('When copying and pasting', () => {
|
|||
const testOffsetY = 3000
|
||||
|
||||
const { w: screenWidth, h: screenHeight } = editor.viewportScreenBounds
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
@ -280,7 +292,11 @@ describe('When copying and pasting', () => {
|
|||
|
||||
const testOffsetX = 100
|
||||
const testOffsetY = 100
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
@ -306,7 +322,11 @@ describe('When copying and pasting', () => {
|
|||
|
||||
const testOffsetX = 1800
|
||||
const testOffsetY = 0
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
@ -335,7 +355,11 @@ describe('When copying and pasting', () => {
|
|||
const testOffsetY = 3000
|
||||
|
||||
const { w: screenWidth, h: screenHeight } = editor.viewportScreenBounds
|
||||
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
||||
editor.setCamera({
|
||||
x: editor.camera.x - testOffsetX,
|
||||
y: editor.camera.y - testOffsetY,
|
||||
z: editor.zoomLevel,
|
||||
})
|
||||
|
||||
editor.paste()
|
||||
const shapesAfter = editor.shapesOnCurrentPage
|
||||
|
|
|
@ -4,19 +4,22 @@ let editor: TestEditor
|
|||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 })
|
||||
})
|
||||
|
||||
describe('viewport.pageToScreen', () => {
|
||||
it('converts correctly', () => {
|
||||
expect(editor.pageToScreen(0, 0)).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen(200, 200)).toMatchObject({
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({
|
||||
x: 200,
|
||||
y: 200,
|
||||
})
|
||||
editor.setCamera(100, 100)
|
||||
expect(editor.pageToScreen(200, 200)).toMatchObject({
|
||||
editor.setCamera({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({
|
||||
x: 300,
|
||||
y: 300,
|
||||
})
|
||||
})
|
||||
|
||||
// see `screen to page` for paired tests
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ beforeEach(() => {
|
|||
|
||||
describe('When panning', () => {
|
||||
it('Updates the camera', () => {
|
||||
editor.pan(200, 200)
|
||||
editor.pan({ x: 200, y: 200 })
|
||||
editor.expectCameraToBe(200, 200, 1)
|
||||
})
|
||||
|
||||
|
@ -23,7 +23,7 @@ describe('When panning', () => {
|
|||
screenBounds.h
|
||||
)
|
||||
const beforePageBounds = editor.viewportPageBounds.clone()
|
||||
editor.pan(200, 200)
|
||||
editor.pan({ x: 200, y: 200 })
|
||||
expect(editor.viewportScreenBounds).toMatchObject(beforeScreenBounds.toJson())
|
||||
expect(editor.viewportPageBounds.toJson()).toMatchObject(
|
||||
beforePageBounds.translate(new Vec2d(-200, -200)).toJson()
|
||||
|
|
|
@ -8,15 +8,293 @@ beforeEach(() => {
|
|||
|
||||
describe('viewport.screenToPage', () => {
|
||||
it('converts correctly', () => {
|
||||
expect(editor.screenToPage(0, 0)).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage(200, 200)).toMatchObject({
|
||||
x: 200,
|
||||
y: 200,
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
|
||||
})
|
||||
|
||||
it('converts correctly when zoomed', () => {
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 })
|
||||
})
|
||||
|
||||
it('converts correctly when panned', () => {
|
||||
editor.setCamera({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 })
|
||||
})
|
||||
|
||||
it('converts correctly when panned and zoomed', () => {
|
||||
editor.setCamera({ x: 100, y: 100, z: 0.5 })
|
||||
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -400, y: -400 })
|
||||
expect(editor.pageToScreen({ x: -400, y: -400 })).toMatchObject({ x: -100, y: -100 })
|
||||
})
|
||||
|
||||
it('converts correctly when offset', () => {
|
||||
// move the editor's page bounds down and to the left by 100, 100
|
||||
// 0,0 s
|
||||
// +------------------------+
|
||||
// | 100,100 s |
|
||||
// | c-----------------+ |
|
||||
// | | 0,0 p | |
|
||||
// | | | |
|
||||
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
|
||||
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
// 0,0 s
|
||||
// c------------------------+
|
||||
// | 100,100 s |
|
||||
// | +-----------------+ |
|
||||
// | | 100,100 p | |
|
||||
// | | | |
|
||||
|
||||
editor.setCamera({ x: -100, y: -100 }) // -100, -100
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
// 0,0 s no offset, zoom at 50%
|
||||
// c------------------------+
|
||||
// | 0,0 p |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 })
|
||||
})
|
||||
|
||||
it('converts correctly when zoomed out', () => {
|
||||
// camera at zero, screenbounds at zero, but zoom at .5
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 })
|
||||
})
|
||||
|
||||
it('converts correctly when zoomed in', () => {
|
||||
editor.setCamera({ x: 0, y: 0, z: 2 })
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -50, y: -50 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 50, y: 50 })
|
||||
})
|
||||
|
||||
it('converts correctly when zoomed', () => {
|
||||
// camera at zero, screenbounds at zero, but zoom at .5
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
|
||||
// zero point, where page and screen are the same
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 50, y: 50 })
|
||||
expect(editor.screenToPage({ x: 50, y: 50 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
|
||||
it('converts correctly when zoomed and panned', () => {
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
editor.setCamera({ x: 100, y: 100, z: 0.5 })
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 150, y: 150 })
|
||||
expect(editor.screenToPage({ x: 150, y: 150 })).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
// zero point, where page and screen are the same
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.screenToPage({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
|
||||
it('converts correctly when offset', () => {
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 150, y: 150 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -200, y: -200 })
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.screenToPage({ x: 300, y: 300 })).toMatchObject({ x: 400, y: 400 })
|
||||
})
|
||||
|
||||
it('converts correctly when panned', () => {
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
editor.setCamera({ x: 100, y: 100, z: 1 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 300, y: 300 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.screenToPage({ x: 300, y: 300 })).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
|
||||
it('converts correctly when panned and zoomed', () => {
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
|
||||
editor.setCamera({ x: 100, y: 100, z: 0.5 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 150, y: 150 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
|
||||
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 150, y: 150 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.screenToPage({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
|
||||
it('converts correctly when panned and zoomed and offset', () => {
|
||||
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
|
||||
editor.setCamera({ x: 100, y: 100, z: 0.5 })
|
||||
|
||||
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 200, y: 200 })
|
||||
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 250, y: 250 })
|
||||
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 300, y: 300 })
|
||||
|
||||
expect(editor.screenToPage({ x: 200, y: 200 })).toMatchObject({ x: 0, y: 0 })
|
||||
expect(editor.screenToPage({ x: 250, y: 250 })).toMatchObject({ x: 100, y: 100 })
|
||||
expect(editor.screenToPage({ x: 300, y: 300 })).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('viewportPageBounds', () => {
|
||||
it('sets the page bounds', () => {
|
||||
editor.updateInstanceState({
|
||||
screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0, w: 1000, h: 1000 },
|
||||
})
|
||||
editor.setCamera(100, 100)
|
||||
expect(editor.screenToPage(200, 200)).toMatchObject({
|
||||
x: 100,
|
||||
y: 100,
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 })
|
||||
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -0,
|
||||
y: -0,
|
||||
w: 1000,
|
||||
h: 1000,
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the page bounds when camera is zoomed', () => {
|
||||
editor.updateInstanceState({
|
||||
screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0, w: 1000, h: 1000 },
|
||||
})
|
||||
editor.setCamera({ x: 0, y: 0, z: 2 })
|
||||
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -0,
|
||||
y: -0,
|
||||
w: 500,
|
||||
h: 500,
|
||||
})
|
||||
editor.setCamera({ x: 0, y: 0, z: 0.5 })
|
||||
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -0,
|
||||
y: -0,
|
||||
w: 2000,
|
||||
h: 2000,
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the page bounds when camera is panned', () => {
|
||||
editor.updateInstanceState({
|
||||
screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0, w: 1000, h: 1000 },
|
||||
})
|
||||
editor.setCamera({ x: 100, y: 100, z: 1 })
|
||||
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -100,
|
||||
y: -100,
|
||||
w: 1000,
|
||||
h: 1000,
|
||||
maxX: 900,
|
||||
maxY: 900,
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the page bounds when camera is panned and zoomed', () => {
|
||||
editor.updateInstanceState({
|
||||
screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0, w: 1000, h: 1000 },
|
||||
})
|
||||
editor.setCamera({ x: 100, y: 100, z: 2 })
|
||||
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -100,
|
||||
y: -100,
|
||||
w: 500,
|
||||
h: 500,
|
||||
maxX: 400,
|
||||
maxY: 400,
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the page bounds when viewport is offset', () => {
|
||||
editor.updateInstanceState({
|
||||
screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100, w: 1000, h: 1000 },
|
||||
})
|
||||
editor.setCamera({ x: 0, y: 0, z: 2 })
|
||||
|
||||
// changing the screen bounds should not affect the page bounds
|
||||
expect(editor.viewportPageBounds).toMatchObject({
|
||||
x: -0,
|
||||
y: -0,
|
||||
w: 500,
|
||||
h: 500,
|
||||
maxX: 500,
|
||||
maxY: 500,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -18,8 +18,8 @@ describe('When resizing', () => {
|
|||
it('sets the viewport bounds with Editor.resize', () => {
|
||||
editor.setScreenBounds({ x: 100, y: 200, w: 700, h: 600 })
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: 100,
|
||||
y: 200,
|
||||
w: 700,
|
||||
h: 600,
|
||||
})
|
||||
|
@ -31,8 +31,8 @@ describe('When resizing', () => {
|
|||
editor.undo() // this should have no effect
|
||||
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: 100,
|
||||
y: 200,
|
||||
w: 700,
|
||||
h: 600,
|
||||
})
|
||||
|
@ -41,8 +41,8 @@ describe('When resizing', () => {
|
|||
it('clamps bounds to minimim 0,0,1,1', () => {
|
||||
editor.setScreenBounds({ x: -100, y: -200, w: -700, h: 0 })
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: -100,
|
||||
y: -200,
|
||||
w: 1,
|
||||
h: 1,
|
||||
})
|
||||
|
@ -51,18 +51,49 @@ describe('When resizing', () => {
|
|||
|
||||
describe('When center is false', () => {
|
||||
it('keeps the same top left when resized', () => {
|
||||
const a = editor.screenToPage(0, 0)
|
||||
const a = editor.screenToPage({ x: 0, y: 0 })
|
||||
expect(a).toMatchObject({ x: 0, y: 0 })
|
||||
editor.setScreenBounds({ x: 100, y: 200, w: 500, h: 600 }, false)
|
||||
const b = editor.screenToPage(0, 0)
|
||||
expect(a).toMatchObject(b)
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 100,
|
||||
y: 200,
|
||||
w: 500,
|
||||
h: 600,
|
||||
})
|
||||
const b = editor.screenToPage({ x: 0, y: 0 })
|
||||
expect(b).toMatchObject({ x: -100, y: -200 })
|
||||
})
|
||||
|
||||
it('keeps the same top left when resized while panned', () => {
|
||||
editor.setCamera({ x: -100, y: -100, z: 1 })
|
||||
const a = editor.screenToPage({ x: 0, y: 0 })
|
||||
expect(a).toMatchObject({ x: 100, y: 100 })
|
||||
|
||||
editor.setScreenBounds({ x: 100, y: 200, w: 500, h: 600 }, false)
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 100,
|
||||
y: 200,
|
||||
w: 500,
|
||||
h: 600,
|
||||
})
|
||||
const b = editor.screenToPage({ x: 0, y: 0 })
|
||||
expect(b).toMatchObject({ x: 0, y: -100 })
|
||||
})
|
||||
|
||||
it('keeps the same top left when resized while panned / zoomed', () => {
|
||||
editor.setCamera(-100, -100, 1.2)
|
||||
const a = editor.screenToPage(0, 0)
|
||||
editor.setScreenBounds({ x: 100, y: 200, w: 500, h: 600 }, false)
|
||||
const b = editor.screenToPage(0, 0)
|
||||
expect(a).toMatchObject(b)
|
||||
editor.setCamera({ x: -100, y: -100, z: 1 })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
|
||||
editor.setCamera({ x: -100, y: -100, z: 2 })
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 50, y: 50 })
|
||||
|
||||
editor.setScreenBounds({ x: 100, y: 100, w: 500, h: 600 }, false)
|
||||
expect(editor.viewportScreenBounds).toMatchObject({
|
||||
x: 100,
|
||||
y: 100,
|
||||
w: 500,
|
||||
h: 600,
|
||||
})
|
||||
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -75,7 +106,7 @@ describe('When center is true', () => {
|
|||
})
|
||||
|
||||
it('keep the same page center when resized while panned / zoomed', () => {
|
||||
editor.setCamera(-100, -100, 1.2)
|
||||
editor.setCamera({ x: -100, y: -100, z: 1.2 })
|
||||
const a = editor.viewportPageCenter.toJson()
|
||||
editor.setScreenBounds({ x: 100, y: 200, w: 500, h: 600 }, true)
|
||||
const b = editor.viewportPageCenter.toJson()
|
||||
|
|
|
@ -26,16 +26,16 @@ it('zooms by increments', () => {
|
|||
})
|
||||
|
||||
it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => {
|
||||
editor.setCamera(0, 0, (ZOOMS[2] + ZOOMS[3]) / 2)
|
||||
editor.setCamera({ x: 0, y: 0, z: (ZOOMS[2] + ZOOMS[3]) / 2 })
|
||||
editor.zoomIn()
|
||||
expect(editor.zoomLevel).toBe(ZOOMS[4])
|
||||
editor.setCamera(0, 0, (ZOOMS[2] + ZOOMS[3]) / 2 - 0.1)
|
||||
editor.setCamera({ x: 0, y: 0, z: (ZOOMS[2] + ZOOMS[3]) / 2 - 0.1 })
|
||||
editor.zoomIn()
|
||||
expect(editor.zoomLevel).toBe(ZOOMS[3])
|
||||
})
|
||||
|
||||
it('does not zoom when camera is frozen', () => {
|
||||
editor.setCamera(0, 0, 1)
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 })
|
||||
expect(editor.camera).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||
editor.updateInstanceState({ canMoveCamera: false })
|
||||
editor.zoomIn()
|
||||
|
|
|
@ -23,7 +23,7 @@ it('zooms by increments', () => {
|
|||
})
|
||||
|
||||
it('does not zoom out when camera is frozen', () => {
|
||||
editor.setCamera(0, 0, 1)
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 })
|
||||
expect(editor.camera).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||
editor.updateInstanceState({ canMoveCamera: false })
|
||||
editor.zoomOut()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Box2d } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -11,19 +12,32 @@ describe('When zooming to bounds', () => {
|
|||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
|
||||
editor.setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 })
|
||||
editor.setCamera(0, 0, 1)
|
||||
editor.zoomToBounds(200, 300, 300, 300)
|
||||
expect(editor.viewportPageCenter.toJson()).toCloselyMatchObject({ x: 350, y: 450 })
|
||||
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 500, y: 500 })
|
||||
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 })
|
||||
|
||||
expect(editor.viewportPageBounds).toCloselyMatchObject({
|
||||
x: -0,
|
||||
y: -0,
|
||||
w: 1000,
|
||||
h: 1000,
|
||||
})
|
||||
|
||||
editor.zoomToBounds(new Box2d(200, 300, 300, 300))
|
||||
expect(editor.camera.z).toCloselyMatchObject((1000 - 256) / 300)
|
||||
expect(editor.viewportPageBounds.width).toCloselyMatchObject(1000 / ((1000 - 256) / 300))
|
||||
expect(editor.viewportPageBounds.height).toCloselyMatchObject(1000 / ((1000 - 256) / 300))
|
||||
})
|
||||
})
|
||||
|
||||
it('does not zoom past max', () => {
|
||||
editor.zoomToBounds(0, 0, 1, 1)
|
||||
editor.zoomToBounds(new Box2d(0, 0, 1, 1))
|
||||
expect(editor.zoomLevel).toBe(8)
|
||||
})
|
||||
|
||||
it('does not zoom past min', () => {
|
||||
editor.zoomToBounds(0, 0, 1000000, 100000)
|
||||
editor.zoomToBounds(new Box2d(0, 0, 1000000, 100000))
|
||||
expect(editor.zoomLevel).toBe(0.1)
|
||||
})
|
||||
|
||||
|
@ -31,6 +45,6 @@ it('does not zoom to bounds when camera is frozen', () => {
|
|||
editor.setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 })
|
||||
expect(editor.viewportPageCenter.toJson()).toCloselyMatchObject({ x: 500, y: 500 })
|
||||
editor.updateInstanceState({ canMoveCamera: false })
|
||||
editor.zoomToBounds(200, 300, 300, 300)
|
||||
editor.zoomToBounds(new Box2d(200, 300, 300, 300))
|
||||
expect(editor.viewportPageCenter.toJson()).toCloselyMatchObject({ x: 500, y: 500 })
|
||||
})
|
||||
|
|
|
@ -418,7 +418,7 @@ describe('When pasting into frames...', () => {
|
|||
.select(ids.frame1)
|
||||
.bringToFront(editor.selectedShapeIds)
|
||||
|
||||
editor.setCamera(-2000, -2000, 1)
|
||||
editor.setCamera({ x: -2000, y: -2000, z: 1 })
|
||||
editor.updateRenderingBounds()
|
||||
|
||||
// Copy box 1 (should be out of viewport)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -129,7 +129,7 @@ describe('When brushing arrows', () => {
|
|||
const ids = editor
|
||||
.selectAll()
|
||||
.deleteShapes(editor.selectedShapeIds)
|
||||
.setCamera(0, 0, 1)
|
||||
.setCamera({ x: 0, y: 0, z: 1 })
|
||||
.createShapesFromJsx([
|
||||
<TL.arrow
|
||||
ref="arrow1"
|
||||
|
@ -151,7 +151,7 @@ describe('When brushing arrows', () => {
|
|||
editor
|
||||
.selectAll()
|
||||
.deleteShapes(editor.selectedShapeIds)
|
||||
.setCamera(0, 0, 1)
|
||||
.setCamera({ x: 0, y: 0, z: 1 })
|
||||
.createShapesFromJsx([
|
||||
<TL.arrow
|
||||
ref="arrow1"
|
||||
|
|
Loading…
Reference in a new issue