[fix] page to screen (#1797)

This PR fixes our page to screen conversion.

### Change Type

- [x] `patch` — Bug fix

### Test Plan

1. Drop an image onto the screen while the camera is panned and zoomed.

- [x] Unit Tests
This commit is contained in:
Steve Ruiz 2023-08-06 09:27:28 +01:00 committed by GitHub
parent 8991468446
commit 16e696ed03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 198 deletions

View file

@ -827,9 +827,9 @@ export class Editor extends EventEmitter<TLEventMap> {
moveShapesToPage(shapes: TLShape[], pageId: TLPageId): this; moveShapesToPage(shapes: TLShape[], pageId: TLPageId): this;
// (undocumented) // (undocumented)
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this; moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: CommandHistoryOptions): this; nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented) // (undocumented)
nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: CommandHistoryOptions): this; nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
get onlySelectedShape(): null | TLShape; get onlySelectedShape(): null | TLShape;
get openMenus(): string[]; get openMenus(): string[];
packShapes(shapes: TLShape[], gap: number): this; packShapes(shapes: TLShape[], gap: number): this;
@ -859,9 +859,9 @@ export class Editor extends EventEmitter<TLEventMap> {
registerExternalContentHandler<T extends TLExternalContent_2['type']>(type: T, handler: ((info: T extends TLExternalContent_2['type'] ? TLExternalContent_2 & { registerExternalContentHandler<T extends TLExternalContent_2['type']>(type: T, handler: ((info: T extends TLExternalContent_2['type'] ? TLExternalContent_2 & {
type: T; type: T;
} : TLExternalContent_2) => void) | null): this; } : TLExternalContent_2) => void) | null): this;
renamePage(page: TLPage, name: string, historyOptions?: CommandHistoryOptions): this; renamePage(page: TLPage, name: string, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented) // (undocumented)
renamePage(id: TLPageId, name: string, historyOptions?: CommandHistoryOptions): this; renamePage(id: TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
get renderingBounds(): Box2d; get renderingBounds(): Box2d;
get renderingBoundsExpanded(): Box2d; get renderingBoundsExpanded(): Box2d;
renderingBoundsMargin: number; renderingBoundsMargin: number;
@ -910,9 +910,9 @@ export class Editor extends EventEmitter<TLEventMap> {
sendToBack(ids: TLShapeId[]): this; sendToBack(ids: TLShapeId[]): this;
setCamera(point: VecLike, animation?: TLAnimationOptions): this; setCamera(point: VecLike, animation?: TLAnimationOptions): this;
setCroppingShapeId(id: null | TLShapeId): this; setCroppingShapeId(id: null | TLShapeId): this;
setCurrentPage(page: TLPage, historyOptions?: CommandHistoryOptions): this; setCurrentPage(page: TLPage, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented) // (undocumented)
setCurrentPage(pageId: TLPageId, historyOptions?: CommandHistoryOptions): this; setCurrentPage(pageId: TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCurrentTool(id: string, info?: {}): this; setCurrentTool(id: string, info?: {}): this;
setCursor: (cursor: Partial<TLCursor>) => this; setCursor: (cursor: Partial<TLCursor>) => this;
setEditingShapeId(id: null | TLShapeId): this; setEditingShapeId(id: null | TLShapeId): this;
@ -920,9 +920,9 @@ export class Editor extends EventEmitter<TLEventMap> {
setFocusedGroupId(next: null | TLShapeId): this; setFocusedGroupId(next: null | TLShapeId): this;
setHintingIds(ids: TLShapeId[]): this; setHintingIds(ids: TLShapeId[]): this;
setHoveredShapeId(id: null | TLShapeId): this; setHoveredShapeId(id: null | TLShapeId): this;
setOpacity(opacity: number, historyOptions?: CommandHistoryOptions): this; setOpacity(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
setSelectedShapeIds(ids: TLShapeId[], historyOptions?: CommandHistoryOptions): this; setSelectedShapeIds(ids: TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: CommandHistoryOptions): this; setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
shapeUtils: { shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>; readonly [K in string]?: ShapeUtil<TLUnknownShape>;
}; };
@ -959,14 +959,14 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented) // (undocumented)
ungroupShapes(ids: TLShape[]): this; ungroupShapes(ids: TLShape[]): this;
updateAssets(assets: TLAssetPartial[]): this; updateAssets(assets: TLAssetPartial[]): this;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: CommandHistoryOptions): this; updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLCommandHistoryOptions): this;
updateDocumentSettings(settings: Partial<TLDocument>): this; updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: CommandHistoryOptions): this; updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: CommandHistoryOptions): this; updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions): this;
// @internal // @internal
updateRenderingBounds(): this; updateRenderingBounds(): this;
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: CommandHistoryOptions): this; updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: TLCommandHistoryOptions): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: CommandHistoryOptions): this; updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: TLCommandHistoryOptions): this;
updateViewportScreenBounds(center?: boolean): this; updateViewportScreenBounds(center?: boolean): this;
readonly user: UserPreferencesManager; readonly user: UserPreferencesManager;
get viewportPageBounds(): Box2d; get viewportPageBounds(): Box2d;

View file

@ -105,7 +105,7 @@ import { parentsToChildren } from './derivations/parentsToChildren'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage' import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
import { ClickManager } from './managers/ClickManager' import { ClickManager } from './managers/ClickManager'
import { EnvironmentManager } from './managers/EnvironmentManager' import { EnvironmentManager } from './managers/EnvironmentManager'
import { CommandHistoryOptions, HistoryManager } from './managers/HistoryManager' import { HistoryManager } from './managers/HistoryManager'
import { SideEffectManager } from './managers/SideEffectManager' import { SideEffectManager } from './managers/SideEffectManager'
import { SnapManager } from './managers/SnapManager' import { SnapManager } from './managers/SnapManager'
import { TextManager } from './managers/TextManager' import { TextManager } from './managers/TextManager'
@ -122,6 +122,7 @@ import { SvgExportContext, SvgExportDef } from './types/SvgExportContext'
import { TLContent } from './types/clipboard-types' import { TLContent } from './types/clipboard-types'
import { TLEventMap } from './types/emit-types' import { TLEventMap } from './types/emit-types'
import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types' import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types'
import { TLCommandHistoryOptions } from './types/history-types'
import { OptionalKeys, RequiredKeys } from './types/misc-types' import { OptionalKeys, RequiredKeys } from './types/misc-types'
import { TLResizeHandle } from './types/selection-types' import { TLResizeHandle } from './types/selection-types'
@ -1185,7 +1186,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/ */
updateInstanceState( updateInstanceState(
partial: Partial<Omit<TLInstance, 'currentPageId'>>, partial: Partial<Omit<TLInstance, 'currentPageId'>>,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
): this { ): this {
this._updateInstanceState(partial, { ephemeral: true, squashing: true, ...historyOptions }) this._updateInstanceState(partial, { ephemeral: true, squashing: true, ...historyOptions })
@ -1207,7 +1208,7 @@ export class Editor extends EventEmitter<TLEventMap> {
'updateInstanceState', 'updateInstanceState',
( (
partial: Partial<Omit<TLInstance, 'currentPageId'>>, partial: Partial<Omit<TLInstance, 'currentPageId'>>,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
) => { ) => {
const prev = this.instanceState const prev = this.instanceState
const next = { ...prev, ...partial } const next = { ...prev, ...partial }
@ -1368,7 +1369,7 @@ export class Editor extends EventEmitter<TLEventMap> {
partial: Partial< partial: Partial<
Omit<TLInstancePageState, 'selectedShapeIds' | 'editingShapeId' | 'pageId' | 'focusedGroupId'> Omit<TLInstancePageState, 'selectedShapeIds' | 'editingShapeId' | 'pageId' | 'focusedGroupId'>
>, >,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
): this { ): this {
this._setInstancePageState(partial, historyOptions) this._setInstancePageState(partial, historyOptions)
return this return this
@ -1379,7 +1380,7 @@ export class Editor extends EventEmitter<TLEventMap> {
'setInstancePageState', 'setInstancePageState',
( (
partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
) => { ) => {
const prev = this.store.get(partial.id ?? this.currentPageState.id)! const prev = this.store.get(partial.id ?? this.currentPageState.id)!
return { data: { prev, partial }, ...historyOptions } return { data: { prev, partial }, ...historyOptions }
@ -1417,7 +1418,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
setSelectedShapeIds(ids: TLShapeId[], historyOptions?: CommandHistoryOptions): this { setSelectedShapeIds(ids: TLShapeId[], historyOptions?: TLCommandHistoryOptions): this {
this._setSelectedShapeIds(ids, historyOptions) this._setSelectedShapeIds(ids, historyOptions)
return this return this
} }
@ -1425,7 +1426,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */ /** @internal */
private _setSelectedShapeIds = this.history.createCommand( private _setSelectedShapeIds = this.history.createCommand(
'setSelectedShapeIds', 'setSelectedShapeIds',
(ids: TLShapeId[], historyOptions?: CommandHistoryOptions) => { (ids: TLShapeId[], historyOptions?: TLCommandHistoryOptions) => {
const { selectedShapeIds: prevSelectedShapeIds } = this.currentPageState const { selectedShapeIds: prevSelectedShapeIds } = this.currentPageState
const prevSet = new Set(prevSelectedShapeIds) const prevSet = new Set(prevSelectedShapeIds)
@ -2693,8 +2694,8 @@ export class Editor extends EventEmitter<TLEventMap> {
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
const { x: cx, y: cy, z: cz = 1 } = this.camera const { x: cx, y: cy, z: cz = 1 } = this.camera
return { return {
x: (point.x - screenBounds.x - cx) / cz, x: (point.x - screenBounds.x) / cz - cx,
y: (point.y - screenBounds.y - cy) / cz, y: (point.y - screenBounds.y) / cz - cy,
z: point.z ?? 0.5, z: point.z ?? 0.5,
} }
} }
@ -2716,8 +2717,8 @@ export class Editor extends EventEmitter<TLEventMap> {
const { x: cx, y: cy, z: cz = 1 } = this.camera const { x: cx, y: cy, z: cz = 1 } = this.camera
return { return {
x: point.x * cz + cx + screenBounds.x, x: (point.x + cx) * cz + screenBounds.x,
y: point.y * cz + cy + screenBounds.y, y: (point.y + cy) * cz + screenBounds.y,
z: point.z ?? 0.5, z: point.z ?? 0.5,
} }
} }
@ -3218,9 +3219,9 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
setCurrentPage(page: TLPage, historyOptions?: CommandHistoryOptions): this setCurrentPage(page: TLPage, historyOptions?: TLCommandHistoryOptions): this
setCurrentPage(pageId: TLPageId, historyOptions?: CommandHistoryOptions): this setCurrentPage(pageId: TLPageId, historyOptions?: TLCommandHistoryOptions): this
setCurrentPage(arg: TLPageId | TLPage, historyOptions?: CommandHistoryOptions): this { setCurrentPage(arg: TLPageId | TLPage, historyOptions?: TLCommandHistoryOptions): this {
const pageId = typeof arg === 'string' ? arg : arg.id const pageId = typeof arg === 'string' ? arg : arg.id
this._setCurrentPageId(pageId, historyOptions) this._setCurrentPageId(pageId, historyOptions)
return this return this
@ -3228,7 +3229,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */ /** @internal */
private _setCurrentPageId = this.history.createCommand( private _setCurrentPageId = this.history.createCommand(
'setCurrentPage', 'setCurrentPage',
(pageId: TLPageId, historyOptions?: CommandHistoryOptions) => { (pageId: TLPageId, historyOptions?: TLCommandHistoryOptions) => {
if (!this.store.has(pageId)) { if (!this.store.has(pageId)) {
console.error("Tried to set the current page id to a page that doesn't exist.") console.error("Tried to set the current page id to a page that doesn't exist.")
return return
@ -3295,14 +3296,14 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: CommandHistoryOptions): this { updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions): this {
this._updatePage(partial, historyOptions) this._updatePage(partial, historyOptions)
return this return this
} }
/** @internal */ /** @internal */
private _updatePage = this.history.createCommand( private _updatePage = this.history.createCommand(
'updatePage', 'updatePage',
(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: CommandHistoryOptions) => { (partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions) => {
if (this.instanceState.isReadonly) return null if (this.instanceState.isReadonly) return null
const prev = this.getPage(partial.id) const prev = this.getPage(partial.id)
@ -3513,9 +3514,9 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
renamePage(page: TLPage, name: string, historyOptions?: CommandHistoryOptions): this renamePage(page: TLPage, name: string, historyOptions?: TLCommandHistoryOptions): this
renamePage(id: TLPageId, name: string, historyOptions?: CommandHistoryOptions): this renamePage(id: TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this
renamePage(arg: TLPageId | TLPage, name: string, historyOptions?: CommandHistoryOptions) { renamePage(arg: TLPageId | TLPage, name: string, historyOptions?: TLCommandHistoryOptions) {
const id = typeof arg === 'string' ? arg : arg.id const id = typeof arg === 'string' ? arg : arg.id
if (this.instanceState.isReadonly) return this if (this.instanceState.isReadonly) return this
this.updatePage({ id, name }, historyOptions) this.updatePage({ id, name }, historyOptions)
@ -4966,12 +4967,12 @@ export class Editor extends EventEmitter<TLEventMap> {
* @param direction - The direction in which to move the shapes. * @param direction - The direction in which to move the shapes.
* @param historyOptions - (optional) The history options for the change. * @param historyOptions - (optional) The history options for the change.
*/ */
nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: CommandHistoryOptions): this nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this
nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: CommandHistoryOptions): this nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this
nudgeShapes( nudgeShapes(
arg: TLShapeId[] | TLShape[], arg: TLShapeId[] | TLShape[],
offset: VecLike, offset: VecLike,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
): this { ): this {
const ids = const ids =
typeof arg[0] === 'string' ? (arg as TLShapeId[]) : (arg as TLShape[]).map((s) => s.id) typeof arg[0] === 'string' ? (arg as TLShapeId[]) : (arg as TLShape[]).map((s) => s.id)
@ -6815,7 +6816,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/ */
updateShape<T extends TLUnknownShape>( updateShape<T extends TLUnknownShape>(
partial: TLShapePartial<T> | null | undefined, partial: TLShapePartial<T> | null | undefined,
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
) { ) {
this.updateShapes([partial], historyOptions) this.updateShapes([partial], historyOptions)
return this return this
@ -6836,7 +6837,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/ */
updateShapes<T extends TLUnknownShape>( updateShapes<T extends TLUnknownShape>(
partials: (TLShapePartial<T> | null | undefined)[], partials: (TLShapePartial<T> | null | undefined)[],
historyOptions?: CommandHistoryOptions historyOptions?: TLCommandHistoryOptions
) { ) {
let compactedPartials = compact(partials) let compactedPartials = compact(partials)
if (this.animatingShapes.size > 0) { if (this.animatingShapes.size > 0) {
@ -6859,7 +6860,10 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */ /** @internal */
private _updateShapes = this.history.createCommand( private _updateShapes = this.history.createCommand(
'updateShapes', 'updateShapes',
(_partials: (TLShapePartial | null | undefined)[], historyOptions?: CommandHistoryOptions) => { (
_partials: (TLShapePartial | null | undefined)[],
historyOptions?: TLCommandHistoryOptions
) => {
if (this.instanceState.isReadonly) return null if (this.instanceState.isReadonly) return null
const partials = compact(_partials) const partials = compact(_partials)
@ -7205,7 +7209,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive. * @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive.
* @param historyOptions - The history options for the change. * @param historyOptions - The history options for the change.
*/ */
setOpacity(opacity: number, historyOptions?: CommandHistoryOptions): this { setOpacity(opacity: number, historyOptions?: TLCommandHistoryOptions): this {
this.history.batch(() => { this.history.batch(() => {
if (this.isIn('select')) { if (this.isIn('select')) {
const { const {
@ -7269,7 +7273,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: CommandHistoryOptions): this { setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this {
this.history.batch(() => { this.history.batch(() => {
if (this.isIn('select')) { if (this.isIn('select')) {
const { const {

View file

@ -1,4 +1,5 @@
import { CommandHistoryOptions, HistoryManager } from './HistoryManager' import { TLCommandHistoryOptions } from '../types/history-types'
import { HistoryManager } from './HistoryManager'
import { stack } from './Stack' import { stack } from './Stack'
function createCounterHistoryManager() { function createCounterHistoryManager() {
@ -291,8 +292,8 @@ describe('history options', () => {
let manager: HistoryManager<any> let manager: HistoryManager<any>
let state: { a: number; b: number } let state: { a: number; b: number }
let setA: (n: number, historyOptions?: CommandHistoryOptions) => any let setA: (n: number, historyOptions?: TLCommandHistoryOptions) => any
let setB: (n: number, historyOptions?: CommandHistoryOptions) => any let setB: (n: number, historyOptions?: TLCommandHistoryOptions) => any
beforeEach(() => { beforeEach(() => {
manager = new HistoryManager({ emit: () => void null }, () => { manager = new HistoryManager({ emit: () => void null }, () => {
@ -306,7 +307,7 @@ describe('history options', () => {
setA = manager.createCommand( setA = manager.createCommand(
'setA', 'setA',
(n: number, historyOptions?: CommandHistoryOptions) => ({ (n: number, historyOptions?: TLCommandHistoryOptions) => ({
data: { next: n, prev: state.a }, data: { next: n, prev: state.a },
...historyOptions, ...historyOptions,
}), }),
@ -323,7 +324,7 @@ describe('history options', () => {
setB = manager.createCommand( setB = manager.createCommand(
'setB', 'setB',
(n: number, historyOptions?: CommandHistoryOptions) => ({ (n: number, historyOptions?: TLCommandHistoryOptions) => ({
data: { next: n, prev: state.b }, data: { next: n, prev: state.b },
...historyOptions, ...historyOptions,
}), }),

View file

@ -1,29 +1,13 @@
import { atom, transact } from '@tldraw/state' import { atom, transact } from '@tldraw/state'
import { devFreeze } from '@tldraw/store' import { devFreeze } from '@tldraw/store'
import { uniqueId } from '../../utils/uniqueId' import { uniqueId } from '../../utils/uniqueId'
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types' import { TLCommandHandler, TLCommandHistoryOptions, TLHistoryEntry } from '../types/history-types'
import { Stack, stack } from './Stack' import { Stack, stack } from './Stack'
/** @public */
export type CommandHistoryOptions = Partial<{
/**
* When true, this command will be squashed with the previous command in the undo / redo stack.
*/
squashing: boolean
/**
* When true, this command will not add anything to the undo / redo stack. Its change will never be undone or redone.
*/
ephemeral: boolean
/**
* When true, adding this this command will not clear out the redo stack.
*/
preservesRedoStack: boolean
}>
type CommandFn<Data> = (...args: any[]) => type CommandFn<Data> = (...args: any[]) =>
| ({ | ({
data: Data data: Data
} & CommandHistoryOptions) } & TLCommandHistoryOptions)
| null | null
| undefined | undefined
| void | void

View file

@ -1,3 +1,19 @@
/** @public */
export type TLCommandHistoryOptions = Partial<{
/**
* When true, this command will be squashed with the previous command in the undo / redo stack.
*/
squashing: boolean
/**
* When true, this command will not add anything to the undo / redo stack. Its change will never be undone or redone.
*/
ephemeral: boolean
/**
* When true, adding this this command will not clear out the redo stack.
*/
preservesRedoStack: boolean
}>
/** @public */ /** @public */
export type TLHistoryMark = { export type TLHistoryMark = {
type: 'STOP' type: 'STOP'

View file

@ -1,3 +1,4 @@
import { VecLike } from '@tldraw/editor'
import { TestEditor } from '../TestEditor' import { TestEditor } from '../TestEditor'
let editor: TestEditor let editor: TestEditor
@ -6,126 +7,70 @@ beforeEach(() => {
editor = new TestEditor() editor = new TestEditor()
}) })
function checkScreenPage(screen: VecLike, page: VecLike) {
const pageResult = editor.screenToPage(screen)
expect(pageResult).toMatchObject(page)
const screenResult = editor.pageToScreen(pageResult)
expect(screenResult).toMatchObject(screen)
}
describe('viewport.screenToPage', () => { describe('viewport.screenToPage', () => {
it('converts correctly', () => { it('converts correctly', () => {
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 0, y: 0 }, { x: 0, y: 0 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 100, y: 100 }, { x: 100, y: 100 })
checkScreenPage({ x: -100, y: -100 }, { 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 })
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: -100, y: -100 })
}) })
it('converts correctly when zoomed', () => { it('converts correctly when zoomed', () => {
editor.setCamera({ x: 0, y: 0, z: 0.5 }) editor.setCamera({ x: 0, y: 0, z: 0.5 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 0, y: 0 }, { x: 0, y: 0 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 100, y: 100 }, { x: 200, y: 200 })
checkScreenPage({ x: -100, y: -100 }, { x: -200, y: -200 })
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', () => { it('converts correctly when panned', () => {
editor.setCamera({ x: 100, y: 100 }) editor.setCamera({ x: 100, y: 100 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -100, y: -100 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 100, y: 100 }, { x: 0, y: 0 })
checkScreenPage({ x: -100, y: -100 }, { x: -200, y: -200 })
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', () => { it('converts correctly when panned and zoomed', () => {
editor.setCamera({ x: 100, y: 100, z: 0.5 }) editor.setCamera({ x: 100, y: 100, z: 0.5 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -200, y: -200 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 100, y: 100 }, { x: 100, y: 100 })
checkScreenPage({ x: -100, y: -100 }, { x: -300, y: -300 })
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: -150, y: -150 }, { x: -400, y: -400 })
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', () => { 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 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: -100, y: -100 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: -100, y: -100 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: -100, y: -100 }, { x: -200, y: -200 })
checkScreenPage({ x: 100, y: 100 }, { 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', () => { it('converts correctly when zoomed out', () => {
// camera at zero, screenbounds at zero, but zoom at .5 // camera at zero, screenbounds at zero, but zoom at .5
editor.setCamera({ x: 0, y: 0, z: 0.5 }) editor.setCamera({ x: 0, y: 0, z: 0.5 })
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) 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 }) checkScreenPage({ x: 0, y: 0 }, { x: 0, y: 0 })
expect(editor.screenToPage({ x: -100, y: -100 })).toMatchObject({ x: -200, y: -200 }) checkScreenPage({ x: -100, y: -100 }, { x: -200, y: -200 })
expect(editor.pageToScreen({ x: -200, y: -200 })).toMatchObject({ x: -100, y: -100 }) checkScreenPage({ x: 100, y: 100 }, { x: 200, y: 200 })
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', () => { it('converts correctly when zoomed in', () => {
editor.setCamera({ x: 0, y: 0, z: 2 }) editor.setCamera({ x: 0, y: 0, z: 2 })
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) 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 }) checkScreenPage({ x: 0, y: 0 }, { x: 0, y: 0 })
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 50, y: 50 }) checkScreenPage({ x: -100, y: -100 }, { x: -50, y: -50 })
checkScreenPage({ x: 100, y: 100 }, { x: 50, y: 50 })
}) })
it('converts correctly when zoomed', () => { it('converts correctly when zoomed', () => {
@ -133,82 +78,63 @@ describe('viewport.screenToPage', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
editor.setCamera({ x: 0, y: 0, z: 0.5 }) editor.setCamera({ x: 0, y: 0, z: 0.5 })
// zero point, where page and screen are the same checkScreenPage({ x: 0, y: 0 }, { x: 0, y: 0 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: -100, y: -100 }, { x: -200, y: -200 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) checkScreenPage({ x: 100, y: 100 }, { x: 200, y: 200 })
})
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 50, y: 50 }) it('converts correctly when offset and zoomed', () => {
expect(editor.screenToPage({ x: 50, y: 50 })).toMatchObject({ x: 100, y: 100 }) editor.setCamera({ x: 0, y: 0, z: 0.5 })
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 100, y: 100 }) checkScreenPage({ x: 0, y: 0 }, { x: -200, y: -200 })
expect(editor.screenToPage({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 }) checkScreenPage({ x: -100, y: -100 }, { x: -400, y: -400 })
checkScreenPage({ x: 100, y: 100 }, { x: 0, y: 0 })
}) })
it('converts correctly when zoomed and panned', () => { it('converts correctly when zoomed and panned', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
editor.setCamera({ x: 100, y: 100, z: 0.5 }) 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 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.screenToPage({ x: 150, y: 150 })).toMatchObject({ x: 100, y: 100 }) checkScreenPage({ x: -100, y: -100 }, { x: -300, y: -300 })
checkScreenPage({ x: 100, y: 100 }, { 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', () => { it('converts correctly when offset', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
editor.setCamera({ x: 0, y: 0, z: 0.5 }) editor.setCamera({ x: 0, y: 0, z: 0.5 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 }) checkScreenPage({ x: 0, y: 0 }, { x: -200, y: -200 })
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 150, y: 150 }) checkScreenPage({ x: 100, y: 100 }, { x: 0, y: 0 })
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 }) checkScreenPage({ x: 200, y: 200 }, { 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', () => { it('converts correctly when panned', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
editor.setCamera({ x: 100, y: 100, z: 1 }) editor.setCamera({ x: 100, y: 100, z: 1 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 200, y: 200 }) checkScreenPage({ x: 100, y: 100 }, { x: 0, y: 0 })
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 300, y: 300 }) checkScreenPage({ x: 200, y: 200 }, { x: 100, y: 100 })
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', () => { it('converts correctly when panned and zoomed', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 0, y: 0 } })
editor.setCamera({ x: 100, y: 100, z: 0.5 }) editor.setCamera({ x: 100, y: 100, z: 0.5 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 }) checkScreenPage({ x: 0, y: 0 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 150, y: 150 }) checkScreenPage({ x: 100, y: 100 }, { x: 100, y: 100 })
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 200, y: 200 }) checkScreenPage({ x: 200, y: 200 }, { x: 300, y: 300 })
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', () => { it('converts correctly when panned and zoomed and offset', () => {
editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } }) editor.updateInstanceState({ screenBounds: { ...editor.viewportScreenBounds, x: 100, y: 100 } })
editor.setCamera({ x: 100, y: 100, z: 0.5 }) editor.setCamera({ x: 100, y: 100, z: 0.5 })
expect(editor.pageToScreen({ x: 0, y: 0 })).toMatchObject({ x: 200, y: 200 }) checkScreenPage({ x: 0, y: 0 }, { x: -300, y: -300 })
expect(editor.pageToScreen({ x: 100, y: 100 })).toMatchObject({ x: 250, y: 250 }) checkScreenPage({ x: 100, y: 100 }, { x: -100, y: -100 })
expect(editor.pageToScreen({ x: 200, y: 200 })).toMatchObject({ x: 300, y: 300 }) checkScreenPage({ x: 200, y: 200 }, { x: 100, y: 100 })
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 })
}) })
}) })

View file

@ -84,7 +84,7 @@ describe('When center is false', () => {
editor.setCamera({ x: -100, y: -100, z: 1 }) editor.setCamera({ x: -100, y: -100, z: 1 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 }) expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
editor.setCamera({ x: -100, y: -100, z: 2 }) editor.setCamera({ x: -100, y: -100, z: 2 })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 50, y: 50 }) expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 100, y: 100 })
editor.setScreenBounds({ x: 100, y: 100, w: 500, h: 600 }, false) editor.setScreenBounds({ x: 100, y: 100, w: 500, h: 600 }, false)
expect(editor.viewportScreenBounds).toMatchObject({ expect(editor.viewportScreenBounds).toMatchObject({
@ -93,7 +93,7 @@ describe('When center is false', () => {
w: 500, w: 500,
h: 600, h: 600,
}) })
expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 0, y: 0 }) expect(editor.screenToPage({ x: 0, y: 0 })).toMatchObject({ x: 50, y: 50 })
}) })
}) })