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:
Steve Ruiz 2023-08-02 16:56:33 +01:00 committed by GitHub
parent 507bba82fd
commit 39dbbca90e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1625 additions and 640 deletions

View file

@ -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: {

View file

@ -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
}

View file

@ -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>>

View file

@ -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,
})
}

View file

@ -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),
})
}

View file

@ -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])
})
}

View file

@ -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),
})
}

View file

@ -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,

View file

@ -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',

View file

@ -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',

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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,
}

View file

@ -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 = () => {

View file

@ -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)
}
})
}

View file

@ -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)
}
})

View file

@ -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')

View file

@ -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 })
})

View file

@ -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

View file

@ -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
})

View file

@ -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()

View file

@ -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,
})
})
})

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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 })
})

View file

@ -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

View file

@ -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"