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