Editor commands API / effects (#1778)
This PR shrinks the commands API surface and adds a manager (`CleanupManager`) for side effects. ### Change Type - [x] `major` — Breaking change ### Test Plan Use the app! Especially undo and redo. Our tests are passing but I've found more cases where our coverage fails to catch issues. ### Release Notes - tbd
This commit is contained in:
parent
03514c00c4
commit
e17074a8b3
139 changed files with 3741 additions and 2701 deletions
|
@ -10,7 +10,7 @@ export function sleep(ms: number) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
||||||
// expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(numberOfShapes)
|
// expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(numberOfShapes)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(1)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
|
|
||||||
await page.keyboard.down('Control')
|
await page.keyboard.down('Control')
|
||||||
|
@ -32,7 +32,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.keyboard.press('KeyV')
|
await page.keyboard.press('KeyV')
|
||||||
await page.keyboard.up('Control')
|
await page.keyboard.up('Control')
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(2)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(1)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
|
|
||||||
await page.getByTestId('main.menu').click()
|
await page.getByTestId('main.menu').click()
|
||||||
|
@ -53,7 +53,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.getByTestId('menu-item.edit').click()
|
await page.getByTestId('menu-item.edit').click()
|
||||||
await page.getByTestId('menu-item.paste').click()
|
await page.getByTestId('menu-item.paste').click()
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(2)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(1)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
|
|
||||||
await page.mouse.click(100, 100, { button: 'right' })
|
await page.mouse.click(100, 100, { button: 'right' })
|
||||||
|
@ -73,7 +73,7 @@ test.describe.skip('clipboard tests', () => {
|
||||||
await page.mouse.click(100, 100, { button: 'right' })
|
await page.mouse.click(100, 100, { button: 'right' })
|
||||||
await page.getByTestId('menu-item.paste').click()
|
await page.getByTestId('menu-item.paste').click()
|
||||||
|
|
||||||
expect(await page.evaluate(() => editor.shapesOnCurrentPage.length)).toBe(2)
|
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
|
||||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,7 +24,7 @@ keywords:
|
||||||
|
|
||||||
The [`Editor`](/gen/editor/Editor) class is the main way of controlling tldraw's editor. You can use it to manage the editor's internal state, make changes to the document, or respond to changes that have occurred.
|
The [`Editor`](/gen/editor/Editor) class is the main way of controlling tldraw's editor. You can use it to manage the editor's internal state, make changes to the document, or respond to changes that have occurred.
|
||||||
|
|
||||||
By design, the [`Editor`](/gen/editor/Editor)'s surface area is very large. Almost everything is available through it. Need to create some shapes? Use [`editor.createShapes()`](/gen/editor/Editor#createShapes). Need to delete them? Use [`editor.deleteShapes(editor.selectedShapeIds)`](/gen/editor/Editor#deleteShapes). Need a sorted array of every shape on the current page? Use [`editor.sortedShapesOnCurrentPage`](/gen/editor/Editor#sortedShapesOnCurrentPage).
|
By design, the [`Editor`](/gen/editor/Editor)'s surface area is very large. Almost everything is available through it. Need to create some shapes? Use [`editor.createShapes()`](/gen/editor/Editor#createShapes). Need to delete them? Use [`editor.deleteShapes(editor.selectedShapeIds)`](/gen/editor/Editor#deleteShapes). Need a sorted array of every shape on the current page? Use [`editor.currentPageShapesSorted`](/gen/editor/Editor#currentPageShapesSorted).
|
||||||
|
|
||||||
This page gives a broad idea of how the [`Editor`](/gen/editor/Editor) class is organized and some of the architectural concepts involved. The full reference is available in the [Editor API](/gen/editor/Editor).
|
This page gives a broad idea of how the [`Editor`](/gen/editor/Editor) class is organized and some of the architectural concepts involved. The full reference is available in the [Editor API](/gen/editor/Editor).
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { TLAssetPartial } from '@tldraw/tlschema';
|
||||||
import { TLBaseShape } from '@tldraw/tlschema';
|
import { TLBaseShape } from '@tldraw/tlschema';
|
||||||
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
||||||
import { TLCamera } from '@tldraw/tlschema';
|
import { TLCamera } from '@tldraw/tlschema';
|
||||||
|
import { TLCameraId } from '@tldraw/tlschema';
|
||||||
import { TLCursorType } from '@tldraw/tlschema';
|
import { TLCursorType } from '@tldraw/tlschema';
|
||||||
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
|
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
|
||||||
import { TLDocument } from '@tldraw/tlschema';
|
import { TLDocument } from '@tldraw/tlschema';
|
||||||
|
@ -48,10 +49,13 @@ import { TLHandle } from '@tldraw/tlschema';
|
||||||
import { TLImageAsset } from '@tldraw/tlschema';
|
import { TLImageAsset } from '@tldraw/tlschema';
|
||||||
import { TLInstance } from '@tldraw/tlschema';
|
import { TLInstance } from '@tldraw/tlschema';
|
||||||
import { TLInstancePageState } from '@tldraw/tlschema';
|
import { TLInstancePageState } from '@tldraw/tlschema';
|
||||||
|
import { TLInstancePageStateId } from '@tldraw/tlschema';
|
||||||
import { TLInstancePresence } from '@tldraw/tlschema';
|
import { TLInstancePresence } from '@tldraw/tlschema';
|
||||||
import { TLPage } from '@tldraw/tlschema';
|
import { TLPage } from '@tldraw/tlschema';
|
||||||
import { TLPageId } from '@tldraw/tlschema';
|
import { TLPageId } from '@tldraw/tlschema';
|
||||||
import { TLParentId } from '@tldraw/tlschema';
|
import { TLParentId } from '@tldraw/tlschema';
|
||||||
|
import { TLPointer } from '@tldraw/tlschema';
|
||||||
|
import { TLPointerId } from '@tldraw/tlschema';
|
||||||
import { TLRecord } from '@tldraw/tlschema';
|
import { TLRecord } from '@tldraw/tlschema';
|
||||||
import { TLScribble } from '@tldraw/tlschema';
|
import { TLScribble } from '@tldraw/tlschema';
|
||||||
import { TLShape } from '@tldraw/tlschema';
|
import { TLShape } from '@tldraw/tlschema';
|
||||||
|
@ -530,7 +534,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;
|
||||||
|
@ -561,6 +564,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
bringToFront(ids: TLShapeId[]): this;
|
bringToFront(ids: TLShapeId[]): this;
|
||||||
get camera(): TLCamera;
|
get camera(): TLCamera;
|
||||||
|
get cameraId(): TLCameraId;
|
||||||
get cameraState(): "idle" | "moving";
|
get cameraState(): "idle" | "moving";
|
||||||
cancel(): this;
|
cancel(): this;
|
||||||
cancelDoubleClick(): void;
|
cancelDoubleClick(): void;
|
||||||
|
@ -568,7 +572,9 @@ 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;
|
||||||
|
// (undocumented)
|
||||||
|
readonly cleanup: CleanupManager;
|
||||||
// @internal
|
// @internal
|
||||||
protected _clickManager: ClickManager;
|
protected _clickManager: ClickManager;
|
||||||
get commonBoundsOfAllShapesOnCurrentPage(): Box2d | undefined;
|
get commonBoundsOfAllShapesOnCurrentPage(): Box2d | undefined;
|
||||||
|
@ -577,7 +583,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
crash(error: unknown): void;
|
crash(error: unknown): void;
|
||||||
// @internal
|
// @internal
|
||||||
get crashingError(): unknown;
|
get crashingError(): unknown;
|
||||||
createAssets(assets: TLAsset[]): this;
|
createAsset(asset: TLAsset): this;
|
||||||
|
createAssets(assets: TLAsset[], opts?: {
|
||||||
|
ephemeral?: boolean;
|
||||||
|
squashing?: boolean;
|
||||||
|
}): this;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
||||||
tags: {
|
tags: {
|
||||||
|
@ -592,21 +602,40 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
createPage(title: string, id?: TLPageId, belowPageIndex?: string): this;
|
createPage(title: string, id?: TLPageId, belowPageIndex?: string): this;
|
||||||
createShape<T extends TLUnknownShape>(partial: TLShapePartial<T>, select?: boolean): this;
|
// (undocumented)
|
||||||
createShapes<T extends TLUnknownShape>(partials: TLShapePartial<T>[], select?: boolean): this;
|
createRecords: (partials: OptionalKeys<TLCamera | TLPointer | TLAsset | TLInstancePageState | TLPage | TLShape, "meta">[], opts?: Partial<{
|
||||||
|
squashing: boolean;
|
||||||
|
ephemeral: boolean;
|
||||||
|
preservesRedoStack: boolean;
|
||||||
|
}> | undefined) => this;
|
||||||
|
createShape<T extends TLUnknownShape>(partial: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
||||||
|
createShapes<T extends TLUnknownShape>(partials: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
||||||
get croppingShapeId(): null | TLShapeId;
|
get croppingShapeId(): null | TLShapeId;
|
||||||
get currentPage(): TLPage;
|
get currentPage(): TLPage;
|
||||||
get currentPageId(): TLPageId;
|
get currentPageId(): TLPageId;
|
||||||
|
get currentPageShapeIds(): Set<TLShapeId>;
|
||||||
|
get currentPageShapes(): TLShape[];
|
||||||
|
get currentPageShapesSorted(): TLShape[];
|
||||||
get currentPageState(): TLInstancePageState;
|
get currentPageState(): TLInstancePageState;
|
||||||
|
get currentPageStateId(): TLInstancePageStateId;
|
||||||
get currentTool(): StateNode | undefined;
|
get currentTool(): StateNode | undefined;
|
||||||
get currentToolId(): string;
|
get currentToolId(): string;
|
||||||
|
deleteAsset(assets: TLAsset): this;
|
||||||
|
// (undocumented)
|
||||||
|
deleteAsset(ids: TLAssetId): this;
|
||||||
deleteAssets(assets: TLAsset[]): this;
|
deleteAssets(assets: TLAsset[]): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
deleteAssets(ids: TLAssetId[]): this;
|
deleteAssets(ids: TLAssetId[]): this;
|
||||||
deleteOpenMenu(id: string): this;
|
deleteOpenMenu(id: string): this;
|
||||||
deletePage(page: TLPage): this;
|
deletePage(page: TLPage): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
deletePage(id: TLPageId): this;
|
deletePage(pageId: TLPageId): this;
|
||||||
|
// (undocumented)
|
||||||
|
deleteRecords: (records: (TLCamera | TLPointer | TLAsset | TLInstancePageState | TLPage | TLShape)[] | (TLCameraId | TLInstancePageStateId | TLPointerId | TLAssetId | TLPageId | TLShapeId)[], opts?: Partial<{
|
||||||
|
squashing: boolean;
|
||||||
|
ephemeral: boolean;
|
||||||
|
preservesRedoStack: boolean;
|
||||||
|
}> | undefined) => this;
|
||||||
deleteShape(id: TLShapeId): this;
|
deleteShape(id: TLShapeId): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
deleteShape(shape: TLShape): this;
|
deleteShape(shape: TLShape): this;
|
||||||
|
@ -629,9 +658,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
duplicateShapes(shapes: TLShape[], offset?: VecLike): this;
|
duplicateShapes(shapes: TLShape[], offset?: VecLike): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
duplicateShapes(ids: TLShapeId[], offset?: VecLike): this;
|
duplicateShapes(ids: TLShapeId[], offset?: VecLike): this;
|
||||||
|
// (undocumented)
|
||||||
|
get editingShape(): TLUnknownShape | undefined;
|
||||||
get editingShapeId(): null | TLShapeId;
|
get editingShapeId(): null | TLShapeId;
|
||||||
|
readonly environment: EnvironmentManager;
|
||||||
get erasingShapeIds(): TLShapeId[];
|
get erasingShapeIds(): TLShapeId[];
|
||||||
get erasingShapeIdsSet(): Set<TLShapeId>;
|
// (undocumented)
|
||||||
|
get erasingShapes(): NonNullable<TLShape | undefined>[];
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
externalAssetContentHandlers: {
|
externalAssetContentHandlers: {
|
||||||
[K in TLExternalAssetContent_2['type']]: {
|
[K in TLExternalAssetContent_2['type']]: {
|
||||||
|
@ -712,6 +745,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getPageMask(id: TLShapeId): undefined | VecLike[];
|
getPageMask(id: TLShapeId): undefined | VecLike[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getPageMask(shape: TLShape): undefined | VecLike[];
|
getPageMask(shape: TLShape): undefined | VecLike[];
|
||||||
|
getPageShapeIds(page: TLPage): Set<TLShapeId>;
|
||||||
|
// (undocumented)
|
||||||
|
getPageShapeIds(pageId: TLPageId): Set<TLShapeId>;
|
||||||
|
getPageState(pageId: TLPageId): TLInstancePageState;
|
||||||
getPageTransform(id: TLShapeId): Matrix2d;
|
getPageTransform(id: TLShapeId): Matrix2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getPageTransform(shape: TLShape): Matrix2d;
|
getPageTransform(shape: TLShape): Matrix2d;
|
||||||
|
@ -739,9 +776,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
hitFrameInside?: boolean | undefined;
|
hitFrameInside?: boolean | undefined;
|
||||||
filter?: ((shape: TLShape) => boolean) | undefined;
|
filter?: ((shape: TLShape) => boolean) | undefined;
|
||||||
}): TLShape | undefined;
|
}): TLShape | undefined;
|
||||||
getShapeIdsInPage(page: TLPage): Set<TLShapeId>;
|
|
||||||
// (undocumented)
|
|
||||||
getShapeIdsInPage(pageId: TLPageId): Set<TLShapeId>;
|
|
||||||
getShapesAtPoint(point: VecLike, opts?: {
|
getShapesAtPoint(point: VecLike, opts?: {
|
||||||
margin?: number | undefined;
|
margin?: number | undefined;
|
||||||
hitInside?: boolean | undefined;
|
hitInside?: boolean | undefined;
|
||||||
|
@ -776,6 +810,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
||||||
get hintingShapeIds(): TLShapeId[];
|
get hintingShapeIds(): TLShapeId[];
|
||||||
|
// (undocumented)
|
||||||
|
get hintingShapes(): NonNullable<TLShape | undefined>[];
|
||||||
readonly history: HistoryManager<this>;
|
readonly history: HistoryManager<this>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get hoveredShape(): TLUnknownShape | undefined;
|
get hoveredShape(): TLUnknownShape | undefined;
|
||||||
|
@ -805,12 +841,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
isAncestorSelected(id: TLShapeId): boolean;
|
isAncestorSelected(id: TLShapeId): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isAncestorSelected(shape: TLShape): boolean;
|
isAncestorSelected(shape: TLShape): boolean;
|
||||||
readonly isAndroid: boolean;
|
|
||||||
readonly isChromeForIos: boolean;
|
|
||||||
readonly isFirefox: boolean;
|
|
||||||
isIn(path: string): boolean;
|
isIn(path: string): boolean;
|
||||||
isInAny(...paths: string[]): boolean;
|
isInAny(...paths: string[]): boolean;
|
||||||
readonly isIos: boolean;
|
|
||||||
get isMenuOpen(): boolean;
|
get isMenuOpen(): boolean;
|
||||||
isPointInShape(shape: TLShape, point: VecLike, opts?: {
|
isPointInShape(shape: TLShape, point: VecLike, opts?: {
|
||||||
margin?: number;
|
margin?: number;
|
||||||
|
@ -821,7 +853,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
margin?: number;
|
margin?: number;
|
||||||
hitInside?: boolean;
|
hitInside?: boolean;
|
||||||
}): boolean;
|
}): boolean;
|
||||||
readonly isSafari: boolean;
|
|
||||||
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
|
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isShapeInPage(shapeId: TLShapeId, pageId?: TLPageId): boolean;
|
isShapeInPage(shapeId: TLShapeId, pageId?: TLPageId): boolean;
|
||||||
|
@ -831,7 +862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
isShapeOrAncestorLocked(shape?: TLShape): boolean;
|
isShapeOrAncestorLocked(shape?: TLShape): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isShapeOrAncestorLocked(id?: TLShapeId): boolean;
|
isShapeOrAncestorLocked(id?: TLShapeId): boolean;
|
||||||
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): string;
|
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this;
|
||||||
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;
|
||||||
|
@ -845,13 +876,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;
|
||||||
|
@ -867,11 +898,10 @@ 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, squashing?: boolean): this;
|
|
||||||
// (undocumented)
|
|
||||||
renamePage(id: TLPageId, name: string, squashing?: boolean): this;
|
|
||||||
get renderingBounds(): Box2d;
|
get renderingBounds(): Box2d;
|
||||||
get renderingBoundsExpanded(): Box2d;
|
get renderingBoundsExpanded(): Box2d;
|
||||||
|
// (undocumented)
|
||||||
|
renderingBoundsMargin: number;
|
||||||
get renderingShapes(): {
|
get renderingShapes(): {
|
||||||
id: TLShapeId;
|
id: TLShapeId;
|
||||||
shape: TLShape;
|
shape: TLShape;
|
||||||
|
@ -883,10 +913,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
isInViewport: boolean;
|
isInViewport: boolean;
|
||||||
maskedPageBounds: Box2d | undefined;
|
maskedPageBounds: Box2d | undefined;
|
||||||
}[];
|
}[];
|
||||||
reparentShapes(shapes: TLShape[], parentId: TLParentId, insertIndex?: string): this;
|
reparentShapes(shapes: TLShape[], parentId: TLParentId, opts?: {
|
||||||
|
insertIndex?: string;
|
||||||
|
} & CommandHistoryOptions): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
reparentShapes(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
reparentShapes(ids: TLShapeId[], parentId: TLParentId, opts?: {
|
||||||
resetZoom(point?: Vec2d, opts?: TLAnimationOptions): this;
|
insertIndex?: string;
|
||||||
|
} & CommandHistoryOptions): 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;
|
||||||
|
@ -900,7 +934,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;
|
||||||
|
@ -921,32 +955,29 @@ 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;
|
setCroppingShapeId(id: null | TLShapeId): this;
|
||||||
setCurrentPage(page: TLPage, opts?: TLViewportOptions): this;
|
setCurrentPage(page: TLPage): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setCurrentPage(pageId: TLPageId, opts?: TLViewportOptions): this;
|
setCurrentPage(pageId: TLPageId): this;
|
||||||
setCurrentTool(id: string, info?: {}): this;
|
setCurrentTool(id: string, info?: {}): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setEditingId(id: null | TLShapeId): this;
|
setEditingShapeId(id: null | TLShapeId): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setErasingIds(ids: TLShapeId[]): this;
|
setErasingShapeIds(ids: TLShapeId[]): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setFocusedGroupId(next: TLPageId | TLShapeId): this;
|
setFocusedGroupId(id: TLPageId | TLShapeId): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setHintingIds(ids: TLShapeId[]): this;
|
setHintingShapeIds(ids: TLShapeId[]): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setHoveredId(id: null | TLShapeId): this;
|
setHoveredShapeId(id: null | TLShapeId): this;
|
||||||
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
setOpacity(opacity: number, opts?: CommandHistoryOptions): this;
|
||||||
setSelectedShapeIds(ids: TLShapeId[], squashing?: boolean): this;
|
setSelectedShapeIds(ids: TLShapeId[], squashing?: boolean): this;
|
||||||
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
|
setStyle<T>(style: StyleProp<T>, value: T, opts?: CommandHistoryOptions): this;
|
||||||
get shapeIdsOnCurrentPage(): Set<TLShapeId>;
|
|
||||||
get shapesOnCurrentPage(): TLShape[];
|
|
||||||
shapeUtils: {
|
shapeUtils: {
|
||||||
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
||||||
};
|
};
|
||||||
get sharedOpacity(): SharedStyle<number>;
|
|
||||||
get sharedStyles(): ReadonlySharedStyleMap;
|
get sharedStyles(): ReadonlySharedStyleMap;
|
||||||
slideCamera(opts?: {
|
slideCamera(opts?: {
|
||||||
speed: number;
|
speed: number;
|
||||||
|
@ -955,7 +986,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
speedThreshold?: number | undefined;
|
speedThreshold?: number | undefined;
|
||||||
}): this | undefined;
|
}): this | undefined;
|
||||||
readonly snaps: SnapManager;
|
readonly snaps: SnapManager;
|
||||||
get sortedShapesOnCurrentPage(): TLShape[];
|
|
||||||
stackShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical', gap: number): this;
|
stackShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical', gap: number): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
stackShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
|
stackShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
|
||||||
|
@ -974,19 +1004,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
toggleLock(shapes: TLShape[]): this;
|
toggleLock(shapes: TLShape[]): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toggleLock(ids: TLShapeId[]): this;
|
toggleLock(ids: TLShapeId[]): this;
|
||||||
undo(): HistoryManager<this>;
|
undo(): this;
|
||||||
ungroupShapes(ids: TLShapeId[]): this;
|
ungroupShapes(ids: TLShapeId[]): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
ungroupShapes(ids: TLShape[]): this;
|
ungroupShapes(ids: TLShape[]): this;
|
||||||
updateAssets(assets: TLAssetPartial[]): this;
|
updateAsset(partial: TLAssetPartial, opts?: {
|
||||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, ephemeral?: boolean): this;
|
ephemeral?: boolean;
|
||||||
|
squashing?: boolean;
|
||||||
|
}): this;
|
||||||
|
updateAssets(partials: TLAssetPartial[], opts?: {
|
||||||
|
ephemeral?: boolean;
|
||||||
|
squashing?: boolean;
|
||||||
|
}): this;
|
||||||
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
||||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, ephemeral?: boolean, squashing?: boolean): this;
|
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, opts?: CommandHistoryOptions): this;
|
||||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
||||||
|
updatePageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId'>>, opts?: CommandHistoryOptions): this;
|
||||||
|
// (undocumented)
|
||||||
|
updateRecords: (partials: Partial<TLRecord>[], opts?: Partial<{
|
||||||
|
squashing: boolean;
|
||||||
|
ephemeral: boolean;
|
||||||
|
preservesRedoStack: boolean;
|
||||||
|
}> | undefined) => this;
|
||||||
// @internal
|
// @internal
|
||||||
updateRenderingBounds(): this;
|
updateRenderingBounds(): this;
|
||||||
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, squashing?: boolean): this;
|
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, opts?: CommandHistoryOptions): this;
|
||||||
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], squashing?: boolean): this;
|
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], opts?: CommandHistoryOptions): this;
|
||||||
updateViewportScreenBounds(center?: boolean): this;
|
updateViewportScreenBounds(center?: boolean): this;
|
||||||
readonly user: UserPreferencesManager;
|
readonly user: UserPreferencesManager;
|
||||||
get viewportPageBounds(): Box2d;
|
get viewportPageBounds(): Box2d;
|
||||||
|
@ -996,13 +1039,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)
|
||||||
|
@ -1628,7 +1671,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: {
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
||||||
'schema' in rest
|
'schema' in rest
|
||||||
? rest.schema
|
? rest.schema
|
||||||
: createTLSchema({
|
: createTLSchema({
|
||||||
shapes: shapesOnCurrentPageToShapeMap(checkShapesAndAddCore(rest.shapeUtils)),
|
shapes: currentPageShapesToShapeMap(checkShapesAndAddCore(rest.shapeUtils)),
|
||||||
})
|
})
|
||||||
return new Store({
|
return new Store({
|
||||||
schema,
|
schema,
|
||||||
|
@ -44,7 +44,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function shapesOnCurrentPageToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
|
function currentPageShapesToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
shapeUtils.map((s): [string, SchemaShapeInfo] => [
|
shapeUtils.map((s): [string, SchemaShapeInfo] => [
|
||||||
s.type,
|
s.type,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,19 @@
|
||||||
|
// let editor: Editor
|
||||||
|
// beforeEach(() => {
|
||||||
|
// editor = new Editor({
|
||||||
|
// shapeUtils: [],
|
||||||
|
// tools: [],
|
||||||
|
// store: createTLStore({ shapeUtils: [] }),
|
||||||
|
// getContainer: () => document.body,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
it.todo('Registers an onBeforeCreate handler')
|
||||||
|
it.todo('Registers an onAfterCreate handler')
|
||||||
|
it.todo('Registers an onBeforeChange handler')
|
||||||
|
it.todo('Registers an onAfterChange handler')
|
||||||
|
it.todo('Registers an onBeforeDelete handler')
|
||||||
|
it.todo('Registers an onAfterDelete handler')
|
||||||
|
|
||||||
|
it.todo('Registers a batch start handler')
|
||||||
|
it.todo('Registers a batch complete handler')
|
243
packages/editor/src/lib/editor/managers/CleanupManager.ts
Normal file
243
packages/editor/src/lib/editor/managers/CleanupManager.ts
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import { TLRecord } from '@tldraw/tlschema'
|
||||||
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLBeforeCreateHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => R
|
||||||
|
/** @public */
|
||||||
|
export type TLAfterCreateHandler<R extends TLRecord> = (
|
||||||
|
record: R,
|
||||||
|
source: 'remote' | 'user'
|
||||||
|
) => void
|
||||||
|
/** @public */
|
||||||
|
export type TLBeforeChangeHandler<R extends TLRecord> = (
|
||||||
|
prev: R,
|
||||||
|
next: R,
|
||||||
|
source: 'remote' | 'user'
|
||||||
|
) => R
|
||||||
|
/** @public */
|
||||||
|
export type TLAfterChangeHandler<R extends TLRecord> = (
|
||||||
|
prev: R,
|
||||||
|
next: R,
|
||||||
|
source: 'remote' | 'user'
|
||||||
|
) => void
|
||||||
|
/** @public */
|
||||||
|
export type TLBeforeDeleteHandler<R extends TLRecord> = (
|
||||||
|
record: R,
|
||||||
|
source: 'remote' | 'user'
|
||||||
|
) => void | false
|
||||||
|
/** @public */
|
||||||
|
export type TLAfterDeleteHandler<R extends TLRecord> = (
|
||||||
|
record: R,
|
||||||
|
source: 'remote' | 'user'
|
||||||
|
) => void
|
||||||
|
/** @public */
|
||||||
|
export type TLBatchCompleteHandler = () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cleanup manager (aka a "side effect wrangler and correct state enforcer")
|
||||||
|
* is responsible for making sure that the editor's state is always correct. This
|
||||||
|
* includes things like: deleting a shape if its parent is deleted; unbinding
|
||||||
|
* arrows when their binding target is deleted; etc.
|
||||||
|
*
|
||||||
|
* We could consider moving this to the store instead.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class CleanupManager {
|
||||||
|
constructor(public editor: Editor) {
|
||||||
|
editor.store.onBeforeCreate = (record, source) => {
|
||||||
|
const handlers = this._beforeCreateHandlers[
|
||||||
|
record.typeName
|
||||||
|
] as TLBeforeCreateHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
let r = record
|
||||||
|
for (const handler of handlers) {
|
||||||
|
r = handler(r, source)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.store.onAfterCreate = (record, source) => {
|
||||||
|
const handlers = this._afterCreateHandlers[
|
||||||
|
record.typeName
|
||||||
|
] as TLAfterCreateHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(record, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.store.onBeforeChange = (prev, next, source) => {
|
||||||
|
const handlers = this._beforeChangeHandlers[
|
||||||
|
next.typeName
|
||||||
|
] as TLBeforeChangeHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
let r = next
|
||||||
|
for (const handler of handlers) {
|
||||||
|
r = handler(prev, r, source)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateDepth = 0
|
||||||
|
|
||||||
|
editor.store.onAfterChange = (prev, next, source) => {
|
||||||
|
updateDepth++
|
||||||
|
|
||||||
|
if (updateDepth > 1000) {
|
||||||
|
console.error('[CleanupManager.onAfterChange] Maximum update depth exceeded, bailing out.')
|
||||||
|
} else {
|
||||||
|
const handlers = this._afterChangeHandlers[
|
||||||
|
next.typeName
|
||||||
|
] as TLAfterChangeHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(prev, next, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDepth--
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.store.onBeforeDelete = (record, source) => {
|
||||||
|
const handlers = this._beforeDeleteHandlers[
|
||||||
|
record.typeName
|
||||||
|
] as TLBeforeDeleteHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
if (handler(record, source) === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.store.onAfterDelete = (record, source) => {
|
||||||
|
const handlers = this._afterDeleteHandlers[
|
||||||
|
record.typeName
|
||||||
|
] as TLAfterDeleteHandler<TLRecord>[]
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(record, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.history.onBatchComplete = () => {
|
||||||
|
this._batchCompleteHandlers.forEach((fn) => fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _beforeCreateHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLBeforeCreateHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
private _afterCreateHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLAfterCreateHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
private _beforeChangeHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLBeforeChangeHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
private _afterChangeHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLAfterChangeHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
|
||||||
|
private _beforeDeleteHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLBeforeDeleteHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
|
||||||
|
private _afterDeleteHandlers: Partial<{
|
||||||
|
[K in TLRecord['typeName']]: TLAfterDeleteHandler<TLRecord & { typeName: K }>[]
|
||||||
|
}> = {}
|
||||||
|
|
||||||
|
private _batchCompleteHandlers: TLBatchCompleteHandler[] = []
|
||||||
|
|
||||||
|
registerBeforeCreateHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLBeforeCreateHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._beforeCreateHandlers[typeName] as TLBeforeCreateHandler<any>[]
|
||||||
|
if (!handlers) this._beforeCreateHandlers[typeName] = []
|
||||||
|
this._beforeCreateHandlers[typeName]!.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAfterCreateHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLAfterCreateHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._afterCreateHandlers[typeName] as TLAfterCreateHandler<any>[]
|
||||||
|
if (!handlers) this._afterCreateHandlers[typeName] = []
|
||||||
|
this._afterCreateHandlers[typeName]!.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerBeforeChangeHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLBeforeChangeHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._beforeChangeHandlers[typeName] as TLBeforeChangeHandler<any>[]
|
||||||
|
if (!handlers) this._beforeChangeHandlers[typeName] = []
|
||||||
|
this._beforeChangeHandlers[typeName]!.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAfterChangeHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLAfterChangeHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._afterChangeHandlers[typeName] as TLAfterChangeHandler<any>[]
|
||||||
|
if (!handlers) this._afterChangeHandlers[typeName] = []
|
||||||
|
this._afterChangeHandlers[typeName]!.push(handler as TLAfterChangeHandler<any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerBeforeDeleteHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLBeforeDeleteHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._beforeDeleteHandlers[typeName] as TLBeforeDeleteHandler<any>[]
|
||||||
|
if (!handlers) this._beforeDeleteHandlers[typeName] = []
|
||||||
|
this._beforeDeleteHandlers[typeName]!.push(handler as TLBeforeDeleteHandler<any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAfterDeleteHandler<T extends TLRecord['typeName']>(
|
||||||
|
typeName: T,
|
||||||
|
handler: TLAfterDeleteHandler<TLRecord & { typeName: T }>
|
||||||
|
) {
|
||||||
|
const handlers = this._afterDeleteHandlers[typeName] as TLAfterDeleteHandler<any>[]
|
||||||
|
if (!handlers) this._afterDeleteHandlers[typeName] = []
|
||||||
|
this._afterDeleteHandlers[typeName]!.push(handler as TLAfterDeleteHandler<any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler to be called when a store completes a batch.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* let count = 0
|
||||||
|
*
|
||||||
|
* editor.cleanup.registerBatchCompleteHandler(() => count++)
|
||||||
|
*
|
||||||
|
* editor.selectAll()
|
||||||
|
* expect(count).toBe(1)
|
||||||
|
*
|
||||||
|
* editor.batch(() => {
|
||||||
|
* editor.selectNone()
|
||||||
|
* editor.selectAll()
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* expect(count).toBe(2)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param handler - The handler to call
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
registerBatchCompleteHandler(handler: TLBatchCompleteHandler) {
|
||||||
|
this._batchCompleteHandlers.push(handler)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
|
export class EnvironmentManager {
|
||||||
|
constructor(public editor: Editor) {
|
||||||
|
if (typeof window !== 'undefined' && 'navigator' in window) {
|
||||||
|
this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||||
|
this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i)
|
||||||
|
this.isChromeForIos = /crios.*safari/i.test(navigator.userAgent)
|
||||||
|
this.isFirefox = /firefox/i.test(navigator.userAgent)
|
||||||
|
this.isAndroid = /android/i.test(navigator.userAgent)
|
||||||
|
} else {
|
||||||
|
this.isSafari = false
|
||||||
|
this.isIos = false
|
||||||
|
this.isChromeForIos = false
|
||||||
|
this.isFirefox = false
|
||||||
|
this.isAndroid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is running in Safari.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly isSafari: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is running on iOS.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly isIos: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is running on iOS.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly isChromeForIos: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is running on Firefox.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly isFirefox: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is running on Android.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly isAndroid: boolean
|
||||||
|
}
|
|
@ -2,13 +2,9 @@ import { HistoryManager } from './HistoryManager'
|
||||||
import { stack } from './Stack'
|
import { stack } from './Stack'
|
||||||
|
|
||||||
function createCounterHistoryManager() {
|
function createCounterHistoryManager() {
|
||||||
const manager = new HistoryManager(
|
const manager = new HistoryManager({ emit: () => void null }, () => {
|
||||||
{ emit: () => void null },
|
|
||||||
() => null,
|
|
||||||
() => {
|
|
||||||
return
|
return
|
||||||
}
|
})
|
||||||
)
|
|
||||||
const state = {
|
const state = {
|
||||||
count: 0,
|
count: 0,
|
||||||
name: 'David',
|
name: 'David',
|
||||||
|
@ -251,7 +247,10 @@ describe(HistoryManager, () => {
|
||||||
expect(editor.getAge()).toBe(35)
|
expect(editor.getAge()).toBe(35)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not allow new history entries to be pushed if a command invokes them while doing or undoing', () => {
|
// I had to turn off ignoringUpdates in command's initial "do" in order to make the cleanup methods undoable
|
||||||
|
// If there are other side effects here that we need to know about, then we should add tests for them;
|
||||||
|
// but so far everything seems ok.
|
||||||
|
it.skip('does not allow new history entries to be pushed if a command invokes them while doing or undoing', () => {
|
||||||
editor.incrementTwice()
|
editor.incrementTwice()
|
||||||
expect(editor.history.numUndos).toBe(1)
|
expect(editor.history.numUndos).toBe(1)
|
||||||
expect(editor.getCount()).toBe(2)
|
expect(editor.getCount()).toBe(2)
|
||||||
|
@ -260,7 +259,7 @@ describe(HistoryManager, () => {
|
||||||
expect(editor.history.numUndos).toBe(0)
|
expect(editor.history.numUndos).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not allow new history entries to be pushed if a command invokes them while bailing', () => {
|
it.skip('does not allow new history entries to be pushed if a command invokes them while bailing', () => {
|
||||||
editor.history.mark('0')
|
editor.history.mark('0')
|
||||||
editor.incrementTwice()
|
editor.incrementTwice()
|
||||||
editor.history.mark('2')
|
editor.history.mark('2')
|
||||||
|
|
|
@ -4,13 +4,17 @@ import { uniqueId } from '../../utils/uniqueId'
|
||||||
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
||||||
import { Stack, stack } from './Stack'
|
import { Stack, stack } from './Stack'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type CommandHistoryOptions = Partial<{
|
||||||
|
squashing: boolean
|
||||||
|
ephemeral: boolean
|
||||||
|
preservesRedoStack: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
type CommandFn<Data> = (...args: any[]) =>
|
type CommandFn<Data> = (...args: any[]) =>
|
||||||
| {
|
| ({
|
||||||
data: Data
|
data: Data
|
||||||
squashing?: boolean
|
} & CommandHistoryOptions)
|
||||||
ephemeral?: boolean
|
|
||||||
preservesRedoStack?: boolean
|
|
||||||
}
|
|
||||||
| null
|
| null
|
||||||
| undefined
|
| undefined
|
||||||
| void
|
| void
|
||||||
|
@ -29,7 +33,6 @@ export class HistoryManager<
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly ctx: CTX,
|
private readonly ctx: CTX,
|
||||||
private readonly onBatchComplete: () => void,
|
|
||||||
private readonly annotateError: (error: unknown) => void
|
private readonly annotateError: (error: unknown) => void
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -43,6 +46,8 @@ export class HistoryManager<
|
||||||
return this._redos.value.length
|
return this._redos.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skipHistory = false
|
||||||
|
|
||||||
createCommand = <Name extends string, Constructor extends CommandFn<any>>(
|
createCommand = <Name extends string, Constructor extends CommandFn<any>>(
|
||||||
name: Name,
|
name: Name,
|
||||||
constructor: Constructor,
|
constructor: Constructor,
|
||||||
|
@ -68,13 +73,14 @@ export class HistoryManager<
|
||||||
|
|
||||||
const { data, ephemeral, squashing, preservesRedoStack } = result
|
const { data, ephemeral, squashing, preservesRedoStack } = result
|
||||||
|
|
||||||
this.ignoringUpdates((undos, redos) => {
|
// this.ignoringUpdates((undos, redos) => {
|
||||||
handle.do(data)
|
handle.do(data)
|
||||||
return { undos, redos }
|
// return { undos, redos }
|
||||||
})
|
// })
|
||||||
|
|
||||||
if (!ephemeral) {
|
if (!ephemeral) {
|
||||||
const prev = this._undos.value.head
|
const prev = this._undos.value.head
|
||||||
|
|
||||||
if (
|
if (
|
||||||
squashing &&
|
squashing &&
|
||||||
prev &&
|
prev &&
|
||||||
|
@ -103,6 +109,7 @@ export class HistoryManager<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear the redo stack unless the command explicitly says not to
|
||||||
if (!result.preservesRedoStack) {
|
if (!result.preservesRedoStack) {
|
||||||
this._redos.set(stack())
|
this._redos.set(stack())
|
||||||
}
|
}
|
||||||
|
@ -116,7 +123,19 @@ export class HistoryManager<
|
||||||
return exec
|
return exec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBatchStart = () => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
onBatchComplete = () => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
batch = (fn: () => void) => {
|
batch = (fn: () => void) => {
|
||||||
|
if (this._batchDepth === 0) {
|
||||||
|
this.onBatchStart()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._batchDepth++
|
this._batchDepth++
|
||||||
if (this._batchDepth === 1) {
|
if (this._batchDepth === 1) {
|
||||||
|
|
|
@ -313,15 +313,15 @@ export class SnapManager {
|
||||||
|
|
||||||
let startNode: GapNode, endNode: GapNode
|
let startNode: GapNode, endNode: GapNode
|
||||||
|
|
||||||
const sortedShapesOnCurrentPageHorizontal = this.snappableShapes.sort((a, b) => {
|
const currentPageShapesSortedHorizontal = this.snappableShapes.sort((a, b) => {
|
||||||
return a.pageBounds.minX - b.pageBounds.minX
|
return a.pageBounds.minX - b.pageBounds.minX
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect horizontal gaps
|
// Collect horizontal gaps
|
||||||
for (let i = 0; i < sortedShapesOnCurrentPageHorizontal.length; i++) {
|
for (let i = 0; i < currentPageShapesSortedHorizontal.length; i++) {
|
||||||
startNode = sortedShapesOnCurrentPageHorizontal[i]
|
startNode = currentPageShapesSortedHorizontal[i]
|
||||||
for (let j = i + 1; j < sortedShapesOnCurrentPageHorizontal.length; j++) {
|
for (let j = i + 1; j < currentPageShapesSortedHorizontal.length; j++) {
|
||||||
endNode = sortedShapesOnCurrentPageHorizontal[j]
|
endNode = currentPageShapesSortedHorizontal[j]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// is there space between the boxes
|
// is there space between the boxes
|
||||||
|
@ -358,14 +358,14 @@ export class SnapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect vertical gaps
|
// Collect vertical gaps
|
||||||
const sortedShapesOnCurrentPageVertical = sortedShapesOnCurrentPageHorizontal.sort((a, b) => {
|
const currentPageShapesSortedVertical = currentPageShapesSortedHorizontal.sort((a, b) => {
|
||||||
return a.pageBounds.minY - b.pageBounds.minY
|
return a.pageBounds.minY - b.pageBounds.minY
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let i = 0; i < sortedShapesOnCurrentPageVertical.length; i++) {
|
for (let i = 0; i < currentPageShapesSortedVertical.length; i++) {
|
||||||
startNode = sortedShapesOnCurrentPageVertical[i]
|
startNode = currentPageShapesSortedVertical[i]
|
||||||
for (let j = i + 1; j < sortedShapesOnCurrentPageVertical.length; j++) {
|
for (let j = i + 1; j < currentPageShapesSortedVertical.length; j++) {
|
||||||
endNode = sortedShapesOnCurrentPageVertical[j]
|
endNode = currentPageShapesSortedVertical[j]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// is there space between the boxes
|
// is there space between the boxes
|
||||||
|
|
|
@ -52,12 +52,12 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
component(shape: TLGroupShape) {
|
component(shape: TLGroupShape) {
|
||||||
// Not a class component, but eslint can't tell that :(
|
// Not a class component, but eslint can't tell that :(
|
||||||
const {
|
const {
|
||||||
erasingShapeIdsSet,
|
erasingShapeIds,
|
||||||
currentPageState: { hintingShapeIds, focusedGroupId },
|
currentPageState: { hintingShapeIds, focusedGroupId },
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
const isErasing = erasingShapeIdsSet.has(shape.id)
|
const isErasing = erasingShapeIds.includes(shape.id)
|
||||||
|
|
||||||
const isHintingOtherGroup =
|
const isHintingOtherGroup =
|
||||||
hintingShapeIds.length > 0 &&
|
hintingShapeIds.length > 0 &&
|
||||||
|
|
|
@ -9,7 +9,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onCancel = () => {
|
override onCancel = () => {
|
||||||
|
|
|
@ -29,8 +29,7 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.editor.createShapes<TLBaseBoxShape>(
|
this.editor.createShapes<TLBaseBoxShape>([
|
||||||
[
|
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: shapeType,
|
type: shapeType,
|
||||||
|
@ -41,9 +40,8 @@ export class Pointing extends StateNode {
|
||||||
h: 1,
|
h: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
])
|
||||||
true
|
this.editor.select(id)
|
||||||
)
|
|
||||||
this.editor.setCurrentTool('select.resizing', {
|
this.editor.setCurrentTool('select.resizing', {
|
||||||
...info,
|
...info,
|
||||||
target: 'selection',
|
target: 'selection',
|
||||||
|
|
|
@ -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>>
|
||||||
|
|
|
@ -84,12 +84,14 @@ export function useCanvasEvents() {
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files)
|
const files = Array.from(e.dataTransfer.files)
|
||||||
|
|
||||||
|
// the drop point should be offset by the position of the editor's container
|
||||||
const rect = editor.getContainer().getBoundingClientRect()
|
const rect = editor.getContainer().getBoundingClientRect()
|
||||||
|
const point = editor.screenToPage({ x: e.clientX - rect.x, y: e.clientY - rect.y })
|
||||||
|
|
||||||
await editor.putExternalContent({
|
await editor.putExternalContent({
|
||||||
type: 'files',
|
type: 'files',
|
||||||
files,
|
files,
|
||||||
point: editor.screenToPage(e.clientX - rect.x, e.clientY - rect.y),
|
point,
|
||||||
ignoreParent: false,
|
ignoreParent: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,11 @@ export function useCoarsePointer() {
|
||||||
// This is a workaround for a Firefox bug where we don't correctly
|
// This is a workaround for a Firefox bug where we don't correctly
|
||||||
// detect coarse VS fine pointer. For now, let's assume that you have a fine
|
// detect coarse VS fine pointer. For now, let's assume that you have a fine
|
||||||
// pointer if you're on Firefox on desktop.
|
// pointer if you're on Firefox on desktop.
|
||||||
if (editor.isFirefox && !editor.isAndroid && !editor.isIos) {
|
if (
|
||||||
|
editor.environment.isFirefox &&
|
||||||
|
!editor.environment.isAndroid &&
|
||||||
|
!editor.environment.isIos
|
||||||
|
) {
|
||||||
editor.updateInstanceState({ isCoarsePointer: false })
|
editor.updateInstanceState({ isCoarsePointer: false })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function useZoomCss() {
|
||||||
const setScaleDebounced = debounce(setScale, 100)
|
const setScaleDebounced = debounce(setScale, 100)
|
||||||
|
|
||||||
const scheduler = new EffectScheduler('useZoomCss', () => {
|
const scheduler = new EffectScheduler('useZoomCss', () => {
|
||||||
const numShapes = editor.shapeIdsOnCurrentPage.size
|
const numShapes = editor.currentPageShapeIds.size
|
||||||
if (numShapes < 300) {
|
if (numShapes < 300) {
|
||||||
setScale(editor.zoomLevel)
|
setScale(editor.zoomLevel)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export interface AtomOptions<Value, Diff> {
|
||||||
* ```ts
|
* ```ts
|
||||||
* const name = atom('name', 'John')
|
* const name = atom('name', 'John')
|
||||||
*
|
*
|
||||||
* console.log(name.value) // 'John'
|
* print(name.value) // 'John'
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
|
|
|
@ -26,7 +26,7 @@ type UNINITIALIZED = typeof UNINITIALIZED
|
||||||
* const count = atom('count', 0)
|
* const count = atom('count', 0)
|
||||||
* const double = computed('double', (prevValue) => {
|
* const double = computed('double', (prevValue) => {
|
||||||
* if (isUninitialized(prevValue)) {
|
* if (isUninitialized(prevValue)) {
|
||||||
* console.log('First time!')
|
* print('First time!')
|
||||||
* }
|
* }
|
||||||
* return count.value * 2
|
* return count.value * 2
|
||||||
* })
|
* })
|
||||||
|
@ -296,7 +296,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
|
||||||
* ```ts
|
* ```ts
|
||||||
* const name = atom('name', 'John')
|
* const name = atom('name', 'John')
|
||||||
* const greeting = computed('greeting', () => `Hello ${name.value}!`)
|
* const greeting = computed('greeting', () => `Hello ${name.value}!`)
|
||||||
* console.log(greeting.value) // 'Hello John!'
|
* print(greeting.value) // 'Hello John!'
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* `computed` may also be used as a decorator for creating computed class properties.
|
* `computed` may also be used as a decorator for creating computed class properties.
|
||||||
|
|
|
@ -39,7 +39,7 @@ let stack: CaptureStackFrame | null = null
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* react('log name changes', () => {
|
* react('log name changes', () => {
|
||||||
* console.log(name.value, 'was changed at', unsafe__withoutCapture(() => time.value))
|
* print(name.value, 'was changed at', unsafe__withoutCapture(() => time.value))
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
|
@ -134,7 +134,7 @@ export function maybeCaptureParent(p: Signal<any, any>) {
|
||||||
* const name = atom('name', 'Bob')
|
* const name = atom('name', 'Bob')
|
||||||
* react('greeting', () => {
|
* react('greeting', () => {
|
||||||
* whyAmIRunning()
|
* whyAmIRunning()
|
||||||
* console.log('Hello', name.value)
|
* print('Hello', name.value)
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* name.set('Alice')
|
* name.set('Alice')
|
||||||
|
|
|
@ -143,7 +143,7 @@ export let currentTransaction = null as Transaction | null
|
||||||
* const lastName = atom('Doe')
|
* const lastName = atom('Doe')
|
||||||
*
|
*
|
||||||
* react('greet', () => {
|
* react('greet', () => {
|
||||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // Logs "Hello, John Doe!"
|
* // Logs "Hello, John Doe!"
|
||||||
|
@ -164,7 +164,7 @@ export let currentTransaction = null as Transaction | null
|
||||||
* const lastName = atom('Doe')
|
* const lastName = atom('Doe')
|
||||||
*
|
*
|
||||||
* react('greet', () => {
|
* react('greet', () => {
|
||||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // Logs "Hello, John Doe!"
|
* // Logs "Hello, John Doe!"
|
||||||
|
@ -187,7 +187,7 @@ export let currentTransaction = null as Transaction | null
|
||||||
* const lastName = atom('Doe')
|
* const lastName = atom('Doe')
|
||||||
*
|
*
|
||||||
* react('greet', () => {
|
* react('greet', () => {
|
||||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // Logs "Hello, John Doe!"
|
* // Logs "Hello, John Doe!"
|
||||||
|
|
|
@ -244,6 +244,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
_flushHistory(): void;
|
_flushHistory(): void;
|
||||||
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
|
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
|
||||||
|
// (undocumented)
|
||||||
|
getRecordType: <T extends R>(record: R) => T;
|
||||||
getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
|
getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
|
||||||
has: <K extends IdOf<R>>(id: K) => boolean;
|
has: <K extends IdOf<R>>(id: K) => boolean;
|
||||||
readonly history: Atom<number, RecordsDiff<R>>;
|
readonly history: Atom<number, RecordsDiff<R>>;
|
||||||
|
@ -255,10 +257,12 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
markAsPossiblyCorrupted(): void;
|
markAsPossiblyCorrupted(): void;
|
||||||
mergeRemoteChanges: (fn: () => void) => void;
|
mergeRemoteChanges: (fn: () => void) => void;
|
||||||
onAfterChange?: (prev: R, next: R) => void;
|
onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void;
|
||||||
onAfterCreate?: (record: R) => void;
|
onAfterCreate?: (record: R, source: 'remote' | 'user') => void;
|
||||||
onAfterDelete?: (prev: R) => void;
|
onAfterDelete?: (prev: R, source: 'remote' | 'user') => void;
|
||||||
onBeforeDelete?: (prev: R) => void;
|
onBeforeChange?: (prev: R, next: R, source: 'remote' | 'user') => R;
|
||||||
|
onBeforeCreate?: (next: R, source: 'remote' | 'user') => R;
|
||||||
|
onBeforeDelete?: (prev: R, source: 'remote' | 'user') => false | void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly props: Props;
|
readonly props: Props;
|
||||||
put: (records: R[], phaseOverride?: 'initialize') => void;
|
put: (records: R[], phaseOverride?: 'initialize') => void;
|
||||||
|
|
|
@ -297,13 +297,29 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null))
|
this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback fired after each record's change.
|
||||||
|
*
|
||||||
|
* @param prev - The previous value, if any.
|
||||||
|
* @param next - The next value.
|
||||||
|
*/
|
||||||
|
onBeforeCreate?: (next: R, source: 'remote' | 'user') => R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback fired after a record is created. Use this to perform related updates to other
|
* A callback fired after a record is created. Use this to perform related updates to other
|
||||||
* records in the store.
|
* records in the store.
|
||||||
*
|
*
|
||||||
* @param record - The record to be created
|
* @param record - The record to be created
|
||||||
*/
|
*/
|
||||||
onAfterCreate?: (record: R) => void
|
onAfterCreate?: (record: R, source: 'remote' | 'user') => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback before after each record's change.
|
||||||
|
*
|
||||||
|
* @param prev - The previous value, if any.
|
||||||
|
* @param next - The next value.
|
||||||
|
*/
|
||||||
|
onBeforeChange?: (prev: R, next: R, source: 'remote' | 'user') => R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback fired after each record's change.
|
* A callback fired after each record's change.
|
||||||
|
@ -311,21 +327,21 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
* @param prev - The previous value, if any.
|
* @param prev - The previous value, if any.
|
||||||
* @param next - The next value.
|
* @param next - The next value.
|
||||||
*/
|
*/
|
||||||
onAfterChange?: (prev: R, next: R) => void
|
onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback fired before a record is deleted.
|
* A callback fired before a record is deleted.
|
||||||
*
|
*
|
||||||
* @param prev - The record that will be deleted.
|
* @param prev - The record that will be deleted.
|
||||||
*/
|
*/
|
||||||
onBeforeDelete?: (prev: R) => void
|
onBeforeDelete?: (prev: R, source: 'remote' | 'user') => false | void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback fired after a record is deleted.
|
* A callback fired after a record is deleted.
|
||||||
*
|
*
|
||||||
* @param prev - The record that will be deleted.
|
* @param prev - The record that will be deleted.
|
||||||
*/
|
*/
|
||||||
onAfterDelete?: (prev: R) => void
|
onAfterDelete?: (prev: R, source: 'remote' | 'user') => void
|
||||||
|
|
||||||
// used to avoid running callbacks when rolling back changes in sync client
|
// used to avoid running callbacks when rolling back changes in sync client
|
||||||
private _runCallbacks = true
|
private _runCallbacks = true
|
||||||
|
@ -353,12 +369,18 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
// changes (e.g. additions, deletions, or updates that produce a new value).
|
// changes (e.g. additions, deletions, or updates that produce a new value).
|
||||||
let didChange = false
|
let didChange = false
|
||||||
|
|
||||||
|
const beforeCreate = this.onBeforeCreate && this._runCallbacks ? this.onBeforeCreate : null
|
||||||
|
const beforeUpdate = this.onBeforeChange && this._runCallbacks ? this.onBeforeChange : null
|
||||||
|
const source = this.isMergingRemoteChanges ? 'remote' : 'user'
|
||||||
|
|
||||||
for (let i = 0, n = records.length; i < n; i++) {
|
for (let i = 0, n = records.length; i < n; i++) {
|
||||||
record = records[i]
|
record = records[i]
|
||||||
|
|
||||||
const recordAtom = (map ?? currentMap)[record.id as IdOf<R>]
|
const recordAtom = (map ?? currentMap)[record.id as IdOf<R>]
|
||||||
|
|
||||||
if (recordAtom) {
|
if (recordAtom) {
|
||||||
|
if (beforeUpdate) record = beforeUpdate(recordAtom.value, record, source)
|
||||||
|
|
||||||
// If we already have an atom for this record, update its value.
|
// If we already have an atom for this record, update its value.
|
||||||
|
|
||||||
const initialValue = recordAtom.__unsafe__getWithoutCapture()
|
const initialValue = recordAtom.__unsafe__getWithoutCapture()
|
||||||
|
@ -382,6 +404,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
updates[record.id] = [initialValue, finalValue]
|
updates[record.id] = [initialValue, finalValue]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (beforeCreate) record = beforeCreate(record, source)
|
||||||
|
|
||||||
didChange = true
|
didChange = true
|
||||||
|
|
||||||
// If we don't have an atom, create one.
|
// If we don't have an atom, create one.
|
||||||
|
@ -418,21 +442,23 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
removed: {} as Record<IdOf<R>, R>,
|
removed: {} as Record<IdOf<R>, R>,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this._runCallbacks) {
|
||||||
const { onAfterCreate, onAfterChange } = this
|
const { onAfterCreate, onAfterChange } = this
|
||||||
|
|
||||||
if (onAfterCreate && this._runCallbacks) {
|
if (onAfterCreate) {
|
||||||
// Run the onAfterChange callback for addition.
|
// Run the onAfterChange callback for addition.
|
||||||
Object.values(additions).forEach((record) => {
|
Object.values(additions).forEach((record) => {
|
||||||
onAfterCreate(record)
|
onAfterCreate(record, source)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onAfterChange && this._runCallbacks) {
|
if (onAfterChange) {
|
||||||
// Run the onAfterChange callback for update.
|
// Run the onAfterChange callback for update.
|
||||||
Object.values(updates).forEach(([from, to]) => {
|
Object.values(updates).forEach(([from, to]) => {
|
||||||
onAfterChange(from, to)
|
onAfterChange(from, to, source)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,12 +470,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
*/
|
*/
|
||||||
remove = (ids: IdOf<R>[]): void => {
|
remove = (ids: IdOf<R>[]): void => {
|
||||||
transact(() => {
|
transact(() => {
|
||||||
|
const cancelled = [] as IdOf<R>[]
|
||||||
|
const source = this.isMergingRemoteChanges ? 'remote' : 'user'
|
||||||
|
|
||||||
if (this.onBeforeDelete && this._runCallbacks) {
|
if (this.onBeforeDelete && this._runCallbacks) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const atom = this.atoms.__unsafe__getWithoutCapture()[id]
|
const atom = this.atoms.__unsafe__getWithoutCapture()[id]
|
||||||
if (!atom) continue
|
if (!atom) continue
|
||||||
|
|
||||||
this.onBeforeDelete(atom.value)
|
if (this.onBeforeDelete(atom.value, source) === false) {
|
||||||
|
cancelled.push(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,6 +491,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
let result: typeof atoms | undefined = undefined
|
let result: typeof atoms | undefined = undefined
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
|
if (cancelled.includes(id)) continue
|
||||||
if (!(id in atoms)) continue
|
if (!(id in atoms)) continue
|
||||||
if (!result) result = { ...atoms }
|
if (!result) result = { ...atoms }
|
||||||
if (!removed) removed = {} as Record<IdOf<R>, R>
|
if (!removed) removed = {} as Record<IdOf<R>, R>
|
||||||
|
@ -476,8 +508,12 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
|
|
||||||
// If we have an onAfterChange, run it for each removed record.
|
// If we have an onAfterChange, run it for each removed record.
|
||||||
if (this.onAfterDelete && this._runCallbacks) {
|
if (this.onAfterDelete && this._runCallbacks) {
|
||||||
|
let record: R
|
||||||
for (let i = 0, n = ids.length; i < n; i++) {
|
for (let i = 0, n = ids.length; i < n; i++) {
|
||||||
this.onAfterDelete(removed[ids[i]])
|
record = removed[ids[i]]
|
||||||
|
if (record) {
|
||||||
|
this.onAfterDelete(record, source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -596,6 +632,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
console.error(`Record ${id} not found. This is probably an error`)
|
console.error(`Record ${id} not found. This is probably an error`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.put([updater(atom.__unsafe__getWithoutCapture() as any as RecFromId<K>) as any])
|
this.put([updater(atom.__unsafe__getWithoutCapture() as any as RecFromId<K>) as any])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -752,6 +789,14 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecordType = <T extends R>(record: R): T => {
|
||||||
|
const type = this.schema.types[record.typeName as R['typeName']]
|
||||||
|
if (!type) {
|
||||||
|
throw new Error(`Record type ${record.typeName} not found`)
|
||||||
|
}
|
||||||
|
return type as unknown as T
|
||||||
|
}
|
||||||
|
|
||||||
private _integrityChecker?: () => void | undefined
|
private _integrityChecker?: () => void | undefined
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
|
@ -39,9 +39,9 @@ it('enters the arrow state', () => {
|
||||||
|
|
||||||
describe('When in the idle state', () => {
|
describe('When in the idle state', () => {
|
||||||
it('enters the pointing state and creates a shape on pointer down', () => {
|
it('enters the pointing state and creates a shape on pointer down', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0)
|
editor.setCurrentTool('arrow').pointerDown(0, 0)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
editor.expectPathToBe('root.arrow.pointing')
|
editor.expectPathToBe('root.arrow.pointing')
|
||||||
})
|
})
|
||||||
|
@ -55,18 +55,18 @@ describe('When in the idle state', () => {
|
||||||
|
|
||||||
describe('When in the pointing state', () => {
|
describe('When in the pointing state', () => {
|
||||||
it('cancels on pointer up', () => {
|
it('cancels on pointer up', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerUp(0, 0)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerUp(0, 0)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore)
|
expect(shapesAfter).toBe(shapesBefore)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.arrow.idle')
|
editor.expectPathToBe('root.arrow.idle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('bails on cancel', () => {
|
it('bails on cancel', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).cancel()
|
editor.setCurrentTool('arrow').pointerDown(0, 0).cancel()
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore)
|
expect(shapesAfter).toBe(shapesBefore)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.arrow.idle')
|
editor.expectPathToBe('root.arrow.idle')
|
||||||
|
@ -82,7 +82,7 @@ describe('When in the pointing state', () => {
|
||||||
describe('When dragging the arrow', () => {
|
describe('When dragging the arrow', () => {
|
||||||
it('updates the arrow on pointer move', () => {
|
it('updates the arrow on pointer move', () => {
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10)
|
||||||
const arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
|
@ -97,9 +97,9 @@ describe('When dragging the arrow', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns to select.idle, keeping shape, on pointer up', () => {
|
it('returns to select.idle, keeping shape, on pointer up', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).pointerUp(10, 10)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).pointerUp(10, 10)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.select.idle')
|
editor.expectPathToBe('root.select.idle')
|
||||||
|
@ -107,18 +107,18 @@ describe('When dragging the arrow', () => {
|
||||||
|
|
||||||
it('returns to arrow.idle, keeping shape, on pointer up when tool lock is active', () => {
|
it('returns to arrow.idle, keeping shape, on pointer up when tool lock is active', () => {
|
||||||
editor.updateInstanceState({ isToolLocked: true })
|
editor.updateInstanceState({ isToolLocked: true })
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).pointerUp(10, 10)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).pointerUp(10, 10)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.arrow.idle')
|
editor.expectPathToBe('root.arrow.idle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('bails on cancel', () => {
|
it('bails on cancel', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).cancel()
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).cancel()
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore)
|
expect(shapesAfter).toBe(shapesBefore)
|
||||||
editor.expectPathToBe('root.arrow.idle')
|
editor.expectPathToBe('root.arrow.idle')
|
||||||
})
|
})
|
||||||
|
@ -139,7 +139,7 @@ describe('When pointing a start shape', () => {
|
||||||
// Clear hinting ids when moving away
|
// Clear hinting ids when moving away
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
|
|
||||||
const arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
|
@ -179,7 +179,7 @@ describe('When pointing an end shape', () => {
|
||||||
// Set hinting id when pointing the shape
|
// Set hinting id when pointing the shape
|
||||||
expect(editor.hintingShapeIds.length).toBe(1)
|
expect(editor.hintingShapeIds.length).toBe(1)
|
||||||
|
|
||||||
const arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
|
@ -208,7 +208,7 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
editor.pointerMove(375, 375)
|
editor.pointerMove(375, 375)
|
||||||
|
|
||||||
let arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
let arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
expect(editor.hintingShapeIds.length).toBe(1)
|
expect(editor.hintingShapeIds.length).toBe(1)
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
|
@ -250,7 +250,7 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
editor.pointerMove(375, 0)
|
editor.pointerMove(375, 0)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
|
@ -268,7 +268,7 @@ describe('When pointing an end shape', () => {
|
||||||
editor.pointerMove(325, 325)
|
editor.pointerMove(325, 325)
|
||||||
expect(editor.hintingShapeIds.length).toBe(1)
|
expect(editor.hintingShapeIds.length).toBe(1)
|
||||||
|
|
||||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
|
@ -289,7 +289,7 @@ describe('When pointing an end shape', () => {
|
||||||
// Give time for the velocity to die down
|
// Give time for the velocity to die down
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
|
@ -316,7 +316,7 @@ describe('When pointing an end shape', () => {
|
||||||
editor.inputs.pointerVelocity = new Vec2d(1, 1)
|
editor.inputs.pointerVelocity = new Vec2d(1, 1)
|
||||||
editor.pointerMove(370, 370)
|
editor.pointerMove(370, 370)
|
||||||
|
|
||||||
const arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
expect(editor.hintingShapeIds.length).toBe(1)
|
expect(editor.hintingShapeIds.length).toBe(1)
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ describe('When pointing an end shape', () => {
|
||||||
it('begins precise when moving slowly', () => {
|
it('begins precise when moving slowly', () => {
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0)
|
editor.setCurrentTool('arrow').pointerDown(0, 0)
|
||||||
|
|
||||||
let arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
let arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
editor.expectShapeToMatch(arrow, {
|
||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
|
@ -358,7 +358,7 @@ describe('When pointing an end shape', () => {
|
||||||
editor.inputs.pointerVelocity = new Vec2d(0.001, 0.001)
|
editor.inputs.pointerVelocity = new Vec2d(0.001, 0.001)
|
||||||
editor.pointerMove(375, 375)
|
editor.pointerMove(375, 375)
|
||||||
|
|
||||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
expect(editor.hintingShapeIds.length).toBe(1)
|
expect(editor.hintingShapeIds.length).toBe(1)
|
||||||
|
|
||||||
|
@ -390,7 +390,7 @@ describe('reparenting issue', () => {
|
||||||
editor.pointerMove(100, 100)
|
editor.pointerMove(100, 100)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const arrowId = editor.sortedShapesOnCurrentPage[0].id
|
const arrowId = editor.currentPageShapesSorted[0].id
|
||||||
|
|
||||||
// Now create three shapes
|
// Now create three shapes
|
||||||
editor.createShapes([
|
editor.createShapes([
|
||||||
|
|
|
@ -302,7 +302,7 @@ describe('Other cases when arrow are moved', () => {
|
||||||
.groupShapes(editor.selectedShapeIds)
|
.groupShapes(editor.selectedShapeIds)
|
||||||
|
|
||||||
editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
||||||
let arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
let arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
|
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
|
||||||
assert(arrow.props.end.type === 'binding')
|
assert(arrow.props.end.type === 'binding')
|
||||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
||||||
|
@ -322,7 +322,7 @@ describe('When a shape it rotated', () => {
|
||||||
it('binds correctly', () => {
|
it('binds correctly', () => {
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
||||||
|
|
||||||
const arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
|
|
||||||
expect(editor.getShape(arrow.id)).toMatchObject({
|
expect(editor.getShape(arrow.id)).toMatchObject({
|
||||||
props: {
|
props: {
|
||||||
|
@ -371,8 +371,8 @@ describe('resizing', () => {
|
||||||
.pointerUp()
|
.pointerUp()
|
||||||
.setCurrentTool('select')
|
.setCurrentTool('select')
|
||||||
|
|
||||||
const arrow1 = editor.shapesOnCurrentPage.at(-2)!
|
const arrow1 = editor.currentPageShapes.at(-2)!
|
||||||
const arrow2 = editor.shapesOnCurrentPage.at(-1)!
|
const arrow2 = editor.currentPageShapes.at(-1)!
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.select(arrow1.id, arrow2.id)
|
.select(arrow1.id, arrow2.id)
|
||||||
|
@ -426,8 +426,8 @@ describe('resizing', () => {
|
||||||
.pointerUp()
|
.pointerUp()
|
||||||
.setCurrentTool('select')
|
.setCurrentTool('select')
|
||||||
|
|
||||||
const arrow1 = editor.shapesOnCurrentPage.at(-2)!
|
const arrow1 = editor.currentPageShapes.at(-2)!
|
||||||
const arrow2 = editor.shapesOnCurrentPage.at(-1)!
|
const arrow2 = editor.currentPageShapes.at(-1)!
|
||||||
|
|
||||||
editor.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }])
|
editor.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }])
|
||||||
|
|
||||||
|
@ -551,6 +551,7 @@ describe("an arrow's parents", () => {
|
||||||
})
|
})
|
||||||
// move b outside of frame
|
// move b outside of frame
|
||||||
editor.select(boxBid).translateSelection(200, 0)
|
editor.select(boxBid).translateSelection(200, 0)
|
||||||
|
jest.advanceTimersByTime(500)
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(editor.getShape(arrowId)).toMatchObject({
|
||||||
parentId: editor.currentPageId,
|
parentId: editor.currentPageId,
|
||||||
props: {
|
props: {
|
||||||
|
@ -565,6 +566,10 @@ describe("an arrow's parents", () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
||||||
const arrowId = editor.onlySelectedShape!.id
|
const arrowId = editor.onlySelectedShape!.id
|
||||||
|
|
||||||
|
expect(editor.getShape(boxAid)!.parentId).toBe(frameId)
|
||||||
|
expect(editor.getShape(boxCid)!.parentId).not.toBe(frameId)
|
||||||
|
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(editor.getShape(arrowId)).toMatchObject({
|
||||||
parentId: editor.currentPageId,
|
parentId: editor.currentPageId,
|
||||||
props: {
|
props: {
|
||||||
|
@ -576,6 +581,12 @@ describe("an arrow's parents", () => {
|
||||||
// move c inside of frame
|
// move c inside of frame
|
||||||
editor.select(boxCid).translateSelection(-40, 0)
|
editor.select(boxCid).translateSelection(-40, 0)
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(500)
|
||||||
|
|
||||||
|
expect(editor.getShape(boxAid)!.parentId).toBe(frameId)
|
||||||
|
expect(editor.getShape(boxCid)!.parentId).toBe(frameId)
|
||||||
|
expect(editor.getShape(arrowId)!.parentId).toBe(frameId)
|
||||||
|
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(editor.getShape(arrowId)).toMatchObject({
|
||||||
parentId: frameId,
|
parentId: frameId,
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -495,7 +495,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const changeIndex = React.useMemo<number>(() => {
|
const changeIndex = React.useMemo<number>(() => {
|
||||||
return this.editor.isSafari ? (globalRenderIndex += 1) : 0
|
return this.editor.environment.isSafari ? (globalRenderIndex += 1) : 0
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [shape])
|
}, [shape])
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onCancel = () => {
|
override onCancel = () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { StateNode, TLArrowShape, TLEventHandlers, createShapeId } from '@tldraw/editor'
|
import { StateNode, TLArrowShape, TLEventHandlers, TLHandle, createShapeId } from '@tldraw/editor'
|
||||||
|
|
||||||
export class Pointing extends StateNode {
|
export class Pointing extends StateNode {
|
||||||
static override id = 'pointing'
|
static override id = 'pointing'
|
||||||
|
@ -7,6 +7,8 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
markId = ''
|
markId = ''
|
||||||
|
|
||||||
|
initialEndHandle = {} as TLHandle
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.didTimeout = false
|
this.didTimeout = false
|
||||||
|
|
||||||
|
@ -19,7 +21,7 @@ export class Pointing extends StateNode {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
this.createArrowShape()
|
this.createArrowShape()
|
||||||
} else {
|
} else {
|
||||||
this.editor.setHintingIds([target.id])
|
this.editor.setHintingShapeIds([target.id])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startPreciseTimeout()
|
this.startPreciseTimeout()
|
||||||
|
@ -27,7 +29,7 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.shape = undefined
|
this.shape = undefined
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
this.clearPreciseTimeout()
|
this.clearPreciseTimeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
this.editor.setCurrentTool('select.dragging_handle', {
|
this.editor.setCurrentTool('select.dragging_handle', {
|
||||||
shape: this.shape,
|
shape: this.shape,
|
||||||
handle: this.editor.getHandles(this.shape)!.find((h) => h.id === 'end')!,
|
handle: this.initialEndHandle,
|
||||||
isCreating: true,
|
isCreating: true,
|
||||||
onInteractionEnd: 'arrow',
|
onInteractionEnd: 'arrow',
|
||||||
})
|
})
|
||||||
|
@ -71,7 +73,7 @@ export class Pointing extends StateNode {
|
||||||
// the arrow might not have been created yet!
|
// the arrow might not have been created yet!
|
||||||
this.editor.bailToMark(this.markId)
|
this.editor.bailToMark(this.markId)
|
||||||
}
|
}
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,9 +82,9 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
|
|
||||||
this.markId = this.editor.mark(`creating:${id}`)
|
this.markId = `creating:${id}`
|
||||||
|
|
||||||
this.editor.createShapes<TLArrowShape>([
|
this.editor.mark(this.markId).createShapes<TLArrowShape>([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
|
@ -107,28 +109,27 @@ export class Pointing extends StateNode {
|
||||||
if (change) {
|
if (change) {
|
||||||
const startTerminal = change.props?.start
|
const startTerminal = change.props?.start
|
||||||
if (startTerminal?.type === 'binding') {
|
if (startTerminal?.type === 'binding') {
|
||||||
this.editor.setHintingIds([startTerminal.boundShapeId])
|
this.editor.setHintingShapeIds([startTerminal.boundShapeId])
|
||||||
}
|
}
|
||||||
this.editor.updateShapes([change], true)
|
// squash me
|
||||||
|
this.editor.updateShape(change, { squashing: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the current shape after those changes
|
// Cache the current shape after those changes
|
||||||
this.shape = this.editor.getShape(id)
|
this.shape = this.editor.getShape(id)
|
||||||
this.editor.select(id)
|
this.editor.setSelectedShapeIds([id], true)
|
||||||
|
|
||||||
|
this.initialEndHandle = this.editor.getHandles(this.shape!)!.find((h) => h.id === 'end')!
|
||||||
}
|
}
|
||||||
|
|
||||||
updateArrowShapeEndHandle() {
|
updateArrowShapeEndHandle() {
|
||||||
const shape = this.shape
|
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||||
if (!shape) throw Error(`expected shape`)
|
|
||||||
|
|
||||||
const handles = this.editor.getHandles(shape)
|
|
||||||
if (!handles) throw Error(`expected handles for arrow`)
|
|
||||||
|
|
||||||
// end update
|
// end update
|
||||||
{
|
{
|
||||||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
const shape = this.editor.getShape(this.shape!.id)! as TLArrowShape
|
||||||
const point = this.editor.getPointInShapeSpace(shape, this.editor.inputs.currentPagePoint)
|
const point = this.editor.getPointInShapeSpace(shape, this.editor.inputs.currentPagePoint)
|
||||||
const endHandle = handles.find((h) => h.id === 'end')!
|
const endHandle = this.editor.getHandles(shape)!.find((h) => h.id === 'end')!
|
||||||
const change = util.onHandleChange?.(shape, {
|
const change = util.onHandleChange?.(shape, {
|
||||||
handle: { ...endHandle, x: point.x, y: point.y },
|
handle: { ...endHandle, x: point.x, y: point.y },
|
||||||
isPrecise: false, // sure about that?
|
isPrecise: false, // sure about that?
|
||||||
|
@ -137,28 +138,28 @@ export class Pointing extends StateNode {
|
||||||
if (change) {
|
if (change) {
|
||||||
const endTerminal = change.props?.end
|
const endTerminal = change.props?.end
|
||||||
if (endTerminal?.type === 'binding') {
|
if (endTerminal?.type === 'binding') {
|
||||||
this.editor.setHintingIds([endTerminal.boundShapeId])
|
this.editor.setHintingShapeIds([endTerminal.boundShapeId])
|
||||||
}
|
}
|
||||||
this.editor.updateShapes([change], true)
|
this.editor.updateShape(change, { squashing: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start update
|
// start update
|
||||||
{
|
{
|
||||||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
const shape = this.editor.getShape(this.shape!.id)! as TLArrowShape
|
||||||
const startHandle = handles.find((h) => h.id === 'start')!
|
const startHandle = this.editor.getHandles(shape)!.find((h) => h.id === 'start')!
|
||||||
const change = util.onHandleChange?.(shape, {
|
const change = util.onHandleChange?.(shape, {
|
||||||
handle: { ...startHandle, x: 0, y: 0 },
|
handle: { ...startHandle, x: 0, y: 0 },
|
||||||
isPrecise: this.didTimeout, // sure about that?
|
isPrecise: this.didTimeout, // sure about that?
|
||||||
})
|
})
|
||||||
|
|
||||||
if (change) {
|
if (change) {
|
||||||
this.editor.updateShapes([change], true)
|
this.editor.updateShape(change, { squashing: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the current shape after those changes
|
// Cache the current shape after those changes
|
||||||
this.shape = this.editor.getShape(shape.id)
|
this.shape = this.editor.getShape(this.shape!.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private preciseTimeout = -1
|
private preciseTimeout = -1
|
||||||
|
|
|
@ -364,7 +364,7 @@ export class Drawing extends StateNode {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShapes<TLDrawShape | TLHighlightShape>([shapePartial], true)
|
this.editor.updateShape<TLDrawShape | TLHighlightShape>(shapePartial, { squashing: true })
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -424,7 +424,7 @@ export class Drawing extends StateNode {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShapes([shapePartial], true)
|
this.editor.updateShape(shapePartial, { squashing: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
@ -566,7 +566,7 @@ export class Drawing extends StateNode {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShapes([shapePartial], true)
|
this.editor.updateShape(shapePartial, { squashing: true })
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -611,11 +611,14 @@ export class Drawing extends StateNode {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShapes([shapePartial], true)
|
this.editor.updateShape(shapePartial, { squashing: true })
|
||||||
|
|
||||||
// Set a maximum length for the lines array; after 200 points, complete the line.
|
// Set a maximum length for the lines array; after 200 points, complete the line.
|
||||||
if (newPoints.length > 500) {
|
if (newPoints.length > 500) {
|
||||||
this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
this.editor.updateShape(
|
||||||
|
{ id, type: this.shapeType, props: { isComplete: true } },
|
||||||
|
{ squashing: true }
|
||||||
|
)
|
||||||
|
|
||||||
const { currentPagePoint } = this.editor.inputs
|
const { currentPagePoint } = this.editor.inputs
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onCancel = () => {
|
override onCancel = () => {
|
||||||
|
|
|
@ -12,44 +12,44 @@ afterEach(() => {
|
||||||
|
|
||||||
describe(FrameShapeTool, () => {
|
describe(FrameShapeTool, () => {
|
||||||
it('Creates frame shapes on click-and-drag, supports undo and redo', () => {
|
it('Creates frame shapes on click-and-drag, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('frame')
|
editor.setCurrentTool('frame')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerMove(100, 100)
|
editor.pointerMove(100, 100)
|
||||||
editor.pointerUp(100, 100)
|
editor.pointerUp(100, 100)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('frame')
|
expect(editor.currentPageShapes[0]?.type).toBe('frame')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates frame shapes on click, supports undo and redo', () => {
|
it('Creates frame shapes on click, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('frame')
|
editor.setCurrentTool('frame')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('frame')
|
expect(editor.currentPageShapes[0]?.type).toBe('frame')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,22 +114,22 @@ describe('When in the pointing state', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a frame and returns to select tool on pointer up', () => {
|
it('Creates a frame and returns to select tool on pointer up', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('frame')
|
editor.setCurrentTool('frame')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.select.idle')
|
editor.expectPathToBe('root.select.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
|
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
|
||||||
editor.updateInstanceState({ isToolLocked: true })
|
editor.updateInstanceState({ isToolLocked: true })
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('frame')
|
editor.setCurrentTool('frame')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.frame.idle')
|
editor.expectPathToBe('root.frame.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const FrameLabelInput = forwardRef<
|
||||||
// and sending us back into edit mode
|
// and sending us back into edit mode
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.currentTarget.blur()
|
e.currentTarget.blur()
|
||||||
editor.setEditingId(null)
|
editor.setEditingShapeId(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
|
@ -30,16 +30,7 @@ export const FrameLabelInput = forwardRef<
|
||||||
const value = e.currentTarget.value.trim()
|
const value = e.currentTarget.value.trim()
|
||||||
if (name === value) return
|
if (name === value) return
|
||||||
|
|
||||||
editor.updateShapes(
|
editor.updateShape({ id, type: 'frame', props: { name: value } }, { squashing: true })
|
||||||
[
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: 'frame',
|
|
||||||
props: { name: value },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
true
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[id, editor]
|
[id, editor]
|
||||||
)
|
)
|
||||||
|
@ -53,16 +44,7 @@ export const FrameLabelInput = forwardRef<
|
||||||
const value = e.currentTarget.value
|
const value = e.currentTarget.value
|
||||||
if (name === value) return
|
if (name === value) return
|
||||||
|
|
||||||
editor.updateShapes(
|
editor.updateShape({ id, type: 'frame', props: { name: value } }, { squashing: true })
|
||||||
[
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: 'frame',
|
|
||||||
props: { name: value },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
true
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[id, editor]
|
[id, editor]
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,44 +12,44 @@ afterEach(() => {
|
||||||
|
|
||||||
describe(GeoShapeTool, () => {
|
describe(GeoShapeTool, () => {
|
||||||
it('Creates geo shapes on click-and-drag, supports undo and redo', () => {
|
it('Creates geo shapes on click-and-drag, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('geo')
|
editor.setCurrentTool('geo')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerMove(100, 100)
|
editor.pointerMove(100, 100)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('geo')
|
expect(editor.currentPageShapes[0]?.type).toBe('geo')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates geo shapes on click, supports undo and redo', () => {
|
it('Creates geo shapes on click, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('geo')
|
editor.setCurrentTool('geo')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('geo')
|
expect(editor.currentPageShapes[0]?.type).toBe('geo')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ describe('When in the idle state', () => {
|
||||||
editor.pointerMove(200, 200)
|
editor.pointerMove(200, 200)
|
||||||
editor.pointerUp(200, 200)
|
editor.pointerUp(200, 200)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
|
|
||||||
editor.selectAll()
|
editor.selectAll()
|
||||||
expect(editor.selectedShapes.length).toBe(2)
|
expect(editor.selectedShapes.length).toBe(2)
|
||||||
|
@ -143,22 +143,22 @@ describe('When in the pointing state', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a geo and returns to select tool on pointer up', () => {
|
it('Creates a geo and returns to select tool on pointer up', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('geo')
|
editor.setCurrentTool('geo')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.select.idle')
|
editor.expectPathToBe('root.select.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a geo and returns to geo.idle on pointer up if tool lock is enabled', () => {
|
it('Creates a geo and returns to geo.idle on pointer up if tool lock is enabled', () => {
|
||||||
editor.updateInstanceState({ isToolLocked: true })
|
editor.updateInstanceState({ isToolLocked: true })
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('geo')
|
editor.setCurrentTool('geo')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.geo.idle')
|
editor.expectPathToBe('root.geo.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||||
|
@ -17,7 +20,7 @@ export class Idle extends StateNode {
|
||||||
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
||||||
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
|
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
|
||||||
this.editor.mark('editing shape')
|
this.editor.mark('editing shape')
|
||||||
this.editor.setEditingId(shape.id)
|
this.editor.setEditingShapeId(shape.id)
|
||||||
this.editor.setCurrentTool('select.editing_shape', {
|
this.editor.setCurrentTool('select.editing_shape', {
|
||||||
...info,
|
...info,
|
||||||
target: 'shape',
|
target: 'shape',
|
||||||
|
|
|
@ -15,9 +15,9 @@ it('enters the line state', () => {
|
||||||
|
|
||||||
describe('When in the idle state', () => {
|
describe('When in the idle state', () => {
|
||||||
it('enters the pointing state and creates a shape on pointer down', () => {
|
it('enters the pointing state and creates a shape on pointer down', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' })
|
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' })
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
editor.expectPathToBe('root.line.pointing')
|
editor.expectPathToBe('root.line.pointing')
|
||||||
})
|
})
|
||||||
|
@ -31,18 +31,18 @@ describe('When in the idle state', () => {
|
||||||
|
|
||||||
describe('When in the pointing state', () => {
|
describe('When in the pointing state', () => {
|
||||||
it('createes on pointer up', () => {
|
it('createes on pointer up', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).pointerUp(0, 0)
|
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).pointerUp(0, 0)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.line.idle')
|
editor.expectPathToBe('root.line.idle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('bails on cancel', () => {
|
it('bails on cancel', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).cancel()
|
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).cancel()
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore)
|
expect(shapesAfter).toBe(shapesBefore)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.line.idle')
|
editor.expectPathToBe('root.line.idle')
|
||||||
|
@ -58,7 +58,7 @@ describe('When in the pointing state', () => {
|
||||||
describe('When dragging the line', () => {
|
describe('When dragging the line', () => {
|
||||||
it('updates the line on pointer move', () => {
|
it('updates the line on pointer move', () => {
|
||||||
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).pointerMove(10, 10)
|
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).pointerMove(10, 10)
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
editor.expectShapeToMatch(line, {
|
editor.expectShapeToMatch(line, {
|
||||||
id: line.id,
|
id: line.id,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
@ -75,13 +75,13 @@ describe('When dragging the line', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns to select.idle, keeping shape, on pointer up', () => {
|
it('returns to select.idle, keeping shape, on pointer up', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor
|
editor
|
||||||
.setCurrentTool('line')
|
.setCurrentTool('line')
|
||||||
.pointerDown(0, 0, { target: 'canvas' })
|
.pointerDown(0, 0, { target: 'canvas' })
|
||||||
.pointerMove(10, 10)
|
.pointerMove(10, 10)
|
||||||
.pointerUp(10, 10)
|
.pointerUp(10, 10)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.select.idle')
|
editor.expectPathToBe('root.select.idle')
|
||||||
|
@ -89,26 +89,26 @@ describe('When dragging the line', () => {
|
||||||
|
|
||||||
it('returns to line.idle, keeping shape, on pointer up if tool lock is enabled', () => {
|
it('returns to line.idle, keeping shape, on pointer up if tool lock is enabled', () => {
|
||||||
editor.updateInstanceState({ isToolLocked: true })
|
editor.updateInstanceState({ isToolLocked: true })
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor
|
editor
|
||||||
.setCurrentTool('line')
|
.setCurrentTool('line')
|
||||||
.pointerDown(0, 0, { target: 'canvas' })
|
.pointerDown(0, 0, { target: 'canvas' })
|
||||||
.pointerMove(10, 10)
|
.pointerMove(10, 10)
|
||||||
.pointerUp(10, 10)
|
.pointerUp(10, 10)
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||||
expect(editor.hintingShapeIds.length).toBe(0)
|
expect(editor.hintingShapeIds.length).toBe(0)
|
||||||
editor.expectPathToBe('root.line.idle')
|
editor.expectPathToBe('root.line.idle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('bails on cancel', () => {
|
it('bails on cancel', () => {
|
||||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
const shapesBefore = editor.currentPageShapes.length
|
||||||
editor
|
editor
|
||||||
.setCurrentTool('line')
|
.setCurrentTool('line')
|
||||||
.pointerDown(0, 0, { target: 'canvas' })
|
.pointerDown(0, 0, { target: 'canvas' })
|
||||||
.pointerMove(10, 10)
|
.pointerMove(10, 10)
|
||||||
.cancel()
|
.cancel()
|
||||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
const shapesAfter = editor.currentPageShapes.length
|
||||||
expect(shapesAfter).toBe(shapesBefore)
|
expect(shapesAfter).toBe(shapesBefore)
|
||||||
editor.expectPathToBe('root.line.idle')
|
editor.expectPathToBe('root.line.idle')
|
||||||
})
|
})
|
||||||
|
@ -126,7 +126,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||||
.pointerDown(20, 10, { target: 'canvas' })
|
.pointerDown(20, 10, { target: 'canvas' })
|
||||||
.pointerUp(20, 10)
|
.pointerUp(20, 10)
|
||||||
|
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
||||||
const handles = Object.values(line.props.handles)
|
const handles = Object.values(line.props.handles)
|
||||||
expect(handles.length).toBe(3)
|
expect(handles.length).toBe(3)
|
||||||
|
@ -143,7 +143,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||||
.pointerMove(30, 10)
|
.pointerMove(30, 10)
|
||||||
.pointerUp(30, 10)
|
.pointerUp(30, 10)
|
||||||
|
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
||||||
const handles = Object.values(line.props.handles)
|
const handles = Object.values(line.props.handles)
|
||||||
expect(handles.length).toBe(3)
|
expect(handles.length).toBe(3)
|
||||||
|
@ -161,7 +161,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||||
.pointerMove(30, 10)
|
.pointerMove(30, 10)
|
||||||
.pointerUp(30, 10)
|
.pointerUp(30, 10)
|
||||||
|
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
||||||
const handles = Object.values(line.props.handles)
|
const handles = Object.values(line.props.handles)
|
||||||
expect(handles.length).toBe(3)
|
expect(handles.length).toBe(3)
|
||||||
|
@ -181,7 +181,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||||
.pointerMove(30, 10)
|
.pointerMove(30, 10)
|
||||||
.pointerUp(30, 10)
|
.pointerUp(30, 10)
|
||||||
|
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
||||||
const handles = Object.values(line.props.handles)
|
const handles = Object.values(line.props.handles)
|
||||||
expect(handles.length).toBe(3)
|
expect(handles.length).toBe(3)
|
||||||
|
@ -203,7 +203,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||||
.pointerMove(40, 10)
|
.pointerMove(40, 10)
|
||||||
.pointerUp(40, 10)
|
.pointerUp(40, 10)
|
||||||
|
|
||||||
const line = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
const line = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||||
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
assert(editor.isShapeOfType<TLLineShape>(line, 'line'))
|
||||||
const handles = Object.values(line.props.handles)
|
const handles = Object.values(line.props.handles)
|
||||||
expect(handles.length).toBe(3)
|
expect(handles.length).toBe(3)
|
||||||
|
|
|
@ -62,7 +62,7 @@ describe('Translating', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
const shape = editor.getShape<TLLineShape>(id)!
|
const shape = editor.getShape<TLLineShape>(id)!
|
||||||
shape.rotation = Math.PI / 2
|
editor.updateShape({ ...shape, rotation: Math.PI / 2 })
|
||||||
|
|
||||||
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
|
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
|
||||||
editor.pointerMove(300, 400) // Move shape by 50, 150
|
editor.pointerMove(300, 400) // Move shape by 50, 150
|
||||||
|
@ -195,7 +195,7 @@ describe('Misc', () => {
|
||||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||||
editor.pointerUp().keyUp('Alt')
|
editor.pointerUp().keyUp('Alt')
|
||||||
|
|
||||||
expect(Array.from(editor.shapeIdsOnCurrentPage.values()).length).toEqual(2)
|
expect(Array.from(editor.currentPageShapeIds.values()).length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deletes', () => {
|
it('deletes', () => {
|
||||||
|
@ -207,7 +207,7 @@ describe('Misc', () => {
|
||||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||||
editor.pointerUp().keyUp('Alt')
|
editor.pointerUp().keyUp('Alt')
|
||||||
|
|
||||||
let ids = Array.from(editor.shapeIdsOnCurrentPage.values())
|
let ids = Array.from(editor.currentPageShapeIds.values())
|
||||||
expect(ids.length).toEqual(2)
|
expect(ids.length).toEqual(2)
|
||||||
|
|
||||||
const duplicate = ids.filter((i) => i !== id)[0]
|
const duplicate = ids.filter((i) => i !== id)[0]
|
||||||
|
@ -215,7 +215,7 @@ describe('Misc', () => {
|
||||||
|
|
||||||
editor.deleteShapes(editor.selectedShapeIds)
|
editor.deleteShapes(editor.selectedShapeIds)
|
||||||
|
|
||||||
ids = Array.from(editor.shapeIdsOnCurrentPage.values())
|
ids = Array.from(editor.currentPageShapeIds.values())
|
||||||
expect(ids.length).toEqual(1)
|
expect(ids.length).toEqual(1)
|
||||||
expect(ids[0]).toEqual(id)
|
expect(ids[0]).toEqual(id)
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,7 @@ Object {
|
||||||
"isLocked": false,
|
"isLocked": false,
|
||||||
"meta": Object {},
|
"meta": Object {},
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"parentId": "page:id51",
|
"parentId": "page:id60",
|
||||||
"props": Object {
|
"props": Object {
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
|
|
|
@ -7,7 +7,10 @@ export class Idle extends StateNode {
|
||||||
|
|
||||||
override onEnter = (info: { shapeId: TLShapeId }) => {
|
override onEnter = (info: { shapeId: TLShapeId }) => {
|
||||||
this.shapeId = info.shapeId
|
this.shapeId = info.shapeId
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerDown: TLEventHandlers['onPointerDown'] = () => {
|
override onPointerDown: TLEventHandlers['onPointerDown'] = () => {
|
||||||
|
|
|
@ -30,7 +30,8 @@ export class Pointing extends StateNode {
|
||||||
const shape = info.shapeId && this.editor.getShape<TLLineShape>(info.shapeId)
|
const shape = info.shapeId && this.editor.getShape<TLLineShape>(info.shapeId)
|
||||||
|
|
||||||
if (shape) {
|
if (shape) {
|
||||||
this.markId = this.editor.mark(`creating:${shape.id}`)
|
this.markId = `creating:${shape.id}`
|
||||||
|
this.editor.mark(this.markId)
|
||||||
this.shape = shape
|
this.shape = shape
|
||||||
|
|
||||||
if (inputs.shiftKey) {
|
if (inputs.shiftKey) {
|
||||||
|
@ -85,9 +86,11 @@ export class Pointing extends StateNode {
|
||||||
} else {
|
} else {
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
|
|
||||||
this.markId = this.editor.mark(`creating:${id}`)
|
this.markId = `creating:${id}`
|
||||||
|
|
||||||
this.editor.createShapes<TLLineShape>([
|
this.editor
|
||||||
|
.mark(this.markId)
|
||||||
|
.createShapes<TLLineShape>([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
@ -95,8 +98,8 @@ export class Pointing extends StateNode {
|
||||||
y: currentPagePoint.y,
|
y: currentPagePoint.y,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
.select(id)
|
||||||
|
|
||||||
this.editor.select(id)
|
|
||||||
this.shape = this.editor.getShape(id)!
|
this.shape = this.editor.getShape(id)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,47 +12,47 @@ afterEach(() => {
|
||||||
|
|
||||||
describe(NoteShapeTool, () => {
|
describe(NoteShapeTool, () => {
|
||||||
it('Creates note shapes on click-and-drag, supports undo and redo', () => {
|
it('Creates note shapes on click-and-drag, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('note')
|
editor.setCurrentTool('note')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerMove(100, 100)
|
editor.pointerMove(100, 100)
|
||||||
editor.pointerUp(100, 100)
|
editor.pointerUp(100, 100)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('note')
|
expect(editor.currentPageShapes[0]?.type).toBe('note')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.cancel() // leave edit mode
|
editor.cancel() // leave edit mode
|
||||||
|
|
||||||
editor.undo() // undoes the selection change
|
editor.undo() // undoes the selection change
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates note shapes on click, supports undo and redo', () => {
|
it('Creates note shapes on click, supports undo and redo', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.setCurrentTool('note')
|
editor.setCurrentTool('note')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('note')
|
expect(editor.currentPageShapes[0]?.type).toBe('note')
|
||||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -126,21 +126,21 @@ describe('When in the pointing state', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a note and begins editing on pointer up', () => {
|
it('Creates a note and begins editing on pointer up', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('note')
|
editor.setCurrentTool('note')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.select.editing_shape')
|
editor.expectPathToBe('root.select.editing_shape')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
|
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
|
||||||
editor.updateInstanceState({ isToolLocked: true })
|
editor.updateInstanceState({ isToolLocked: true })
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('note')
|
editor.setCurrentTool('note')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
editor.expectPathToBe('root.note.idle')
|
editor.expectPathToBe('root.note.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onCancel = () => {
|
override onCancel = () => {
|
||||||
|
|
|
@ -65,7 +65,7 @@ export class Pointing extends StateNode {
|
||||||
if (this.editor.instanceState.isToolLocked) {
|
if (this.editor.instanceState.isToolLocked) {
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
} else {
|
} else {
|
||||||
this.editor.setEditingId(this.shape.id)
|
this.editor.setEditingShapeId(this.shape.id)
|
||||||
this.editor.setCurrentTool('select.editing_shape', {
|
this.editor.setCurrentTool('select.editing_shape', {
|
||||||
...this.info,
|
...this.info,
|
||||||
target: 'shape',
|
target: 'shape',
|
||||||
|
@ -87,19 +87,17 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
this.markId = `creating:${id}`
|
this.markId = `creating:${id}`
|
||||||
this.editor.mark(this.markId)
|
this.editor
|
||||||
|
.mark(this.markId)
|
||||||
this.editor.createShapes(
|
.createShapes([
|
||||||
[
|
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'note',
|
type: 'note',
|
||||||
x: originPagePoint.x,
|
x: originPagePoint.x,
|
||||||
y: originPagePoint.y,
|
y: originPagePoint.y,
|
||||||
},
|
},
|
||||||
],
|
])
|
||||||
true
|
.select(id)
|
||||||
)
|
|
||||||
|
|
||||||
const shape = this.editor.getShape<TLNoteShape>(id)!
|
const shape = this.editor.getShape<TLNoteShape>(id)!
|
||||||
const bounds = this.editor.getGeometry(shape).bounds
|
const bounds = this.editor.getGeometry(shape).bounds
|
||||||
|
|
|
@ -250,7 +250,7 @@ function PatternFillDefForCanvas() {
|
||||||
const { defs, isReady } = usePattern()
|
const { defs, isReady } = usePattern()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && editor.isSafari) {
|
if (isReady && editor.environment.isSafari) {
|
||||||
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
||||||
if (htmlLayer) {
|
if (htmlLayer) {
|
||||||
// Wait for `patternContext` to be picked up
|
// Wait for `patternContext` to be picked up
|
||||||
|
|
|
@ -211,8 +211,8 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (isEditableFromHover) {
|
if (isEditableFromHover) {
|
||||||
transact(() => {
|
transact(() => {
|
||||||
editor.setEditingId(id)
|
editor.setEditingShapeId(id)
|
||||||
editor.setHoveredId(id)
|
editor.setHoveredShapeId(id)
|
||||||
editor.setSelectedShapeIds([id])
|
editor.setSelectedShapeIds([id])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ afterEach(() => {
|
||||||
|
|
||||||
describe(TextShapeTool, () => {
|
describe(TextShapeTool, () => {
|
||||||
it('Creates text, edits it, undoes and redoes', () => {
|
it('Creates text, edits it, undoes and redoes', () => {
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.setCurrentTool('text')
|
editor.setCurrentTool('text')
|
||||||
editor.expectToBeIn('text.idle')
|
editor.expectToBeIn('text.idle')
|
||||||
editor.pointerDown(0, 0)
|
editor.pointerDown(0, 0)
|
||||||
|
@ -22,28 +22,28 @@ describe(TextShapeTool, () => {
|
||||||
editor.expectToBeIn('select.editing_shape')
|
editor.expectToBeIn('select.editing_shape')
|
||||||
// This comes from the component, not the state chart
|
// This comes from the component, not the state chart
|
||||||
editor.updateShapes([
|
editor.updateShapes([
|
||||||
{ ...editor.shapesOnCurrentPage[0]!, type: 'text', props: { text: 'Hello' } },
|
{ ...editor.currentPageShapes[0]!, type: 'text', props: { text: 'Hello' } },
|
||||||
])
|
])
|
||||||
// Deselect the editing shape
|
// Deselect the editing shape
|
||||||
editor.cancel()
|
editor.cancel()
|
||||||
editor.expectToBeIn('select.idle')
|
editor.expectToBeIn('select.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
editor.expectShapeToMatch({
|
editor.expectShapeToMatch({
|
||||||
id: editor.shapesOnCurrentPage[0].id,
|
id: editor.currentPageShapes[0].id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
props: { text: 'Hello' },
|
props: { text: 'Hello' },
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
|
|
||||||
editor.expectShapeToMatch({
|
editor.expectShapeToMatch({
|
||||||
id: editor.shapesOnCurrentPage[0].id,
|
id: editor.currentPageShapes[0].id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
props: { text: 'Hello' },
|
props: { text: 'Hello' },
|
||||||
})
|
})
|
||||||
|
@ -71,7 +71,7 @@ describe('When in idle state', () => {
|
||||||
editor.pointerDown(0, 0)
|
editor.pointerDown(0, 0)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
editor.expectToBeIn('select.editing_shape')
|
editor.expectToBeIn('select.editing_shape')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns to select on cancel', () => {
|
it('returns to select on cancel', () => {
|
||||||
|
@ -87,7 +87,7 @@ describe('When in the pointing state', () => {
|
||||||
editor.pointerDown(0, 0)
|
editor.pointerDown(0, 0)
|
||||||
editor.cancel()
|
editor.cancel()
|
||||||
editor.expectToBeIn('text.idle')
|
editor.expectToBeIn('text.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns to idle on interrupt', () => {
|
it('returns to idle on interrupt', () => {
|
||||||
|
@ -96,7 +96,7 @@ describe('When in the pointing state', () => {
|
||||||
editor.expectToBeIn('text.pointing')
|
editor.expectToBeIn('text.pointing')
|
||||||
editor.interrupt()
|
editor.interrupt()
|
||||||
editor.expectToBeIn('text.idle')
|
editor.expectToBeIn('text.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('transitions to select.resizing when dragging and edits on pointer up', () => {
|
it('transitions to select.resizing when dragging and edits on pointer up', () => {
|
||||||
|
@ -105,7 +105,7 @@ describe('When in the pointing state', () => {
|
||||||
editor.pointerMove(10, 10)
|
editor.pointerMove(10, 10)
|
||||||
editor.expectToBeIn('select.resizing')
|
editor.expectToBeIn('select.resizing')
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
editor.expectToBeIn('select.editing_shape')
|
editor.expectToBeIn('select.editing_shape')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -115,8 +115,8 @@ describe('When in the pointing state', () => {
|
||||||
const y = 0
|
const y = 0
|
||||||
editor.pointerDown(x, y)
|
editor.pointerDown(x, y)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
const bounds = editor.getPageBounds(editor.shapesOnCurrentPage[0])!
|
const bounds = editor.getPageBounds(editor.currentPageShapes[0])!
|
||||||
expect(editor.shapesOnCurrentPage[0]).toMatchObject({
|
expect(editor.currentPageShapes[0]).toMatchObject({
|
||||||
x: x - bounds.width / 2,
|
x: x - bounds.width / 2,
|
||||||
y: y - bounds.height / 2,
|
y: y - bounds.height / 2,
|
||||||
})
|
})
|
||||||
|
@ -131,7 +131,7 @@ describe('When resizing', () => {
|
||||||
editor.expectToBeIn('select.resizing')
|
editor.expectToBeIn('select.resizing')
|
||||||
editor.cancel()
|
editor.cancel()
|
||||||
editor.expectToBeIn('text.idle')
|
editor.expectToBeIn('text.idle')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not bails on interrupt while resizing', () => {
|
it('does not bails on interrupt while resizing', () => {
|
||||||
|
@ -140,7 +140,7 @@ describe('When resizing', () => {
|
||||||
editor.pointerMove(100, 100)
|
editor.pointerMove(100, 100)
|
||||||
editor.expectToBeIn('select.resizing')
|
editor.expectToBeIn('select.resizing')
|
||||||
editor.interrupt()
|
editor.interrupt()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preserves the top left when the text has a fixed width', () => {
|
it('preserves the top left when the text has a fixed width', () => {
|
||||||
|
@ -149,7 +149,7 @@ describe('When resizing', () => {
|
||||||
const y = 0
|
const y = 0
|
||||||
editor.pointerDown(x, y)
|
editor.pointerDown(x, y)
|
||||||
editor.pointerMove(x + 100, y + 100)
|
editor.pointerMove(x + 100, y + 100)
|
||||||
expect(editor.shapesOnCurrentPage[0]).toMatchObject({
|
expect(editor.currentPageShapes[0]).toMatchObject({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class Idle extends StateNode {
|
||||||
if (this.editor.isShapeOfType<TLTextShape>(hitShape, 'text')) {
|
if (this.editor.isShapeOfType<TLTextShape>(hitShape, 'text')) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.editor.setSelectedShapeIds([hitShape.id])
|
this.editor.setSelectedShapeIds([hitShape.id])
|
||||||
this.editor.setEditingId(hitShape.id)
|
this.editor.setEditingShapeId(hitShape.id)
|
||||||
this.editor.setCurrentTool('select.editing_shape', {
|
this.editor.setCurrentTool('select.editing_shape', {
|
||||||
...info,
|
...info,
|
||||||
target: 'shape',
|
target: 'shape',
|
||||||
|
@ -38,7 +38,10 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
||||||
|
@ -46,7 +49,7 @@ export class Idle extends StateNode {
|
||||||
const shape = this.editor.selectedShapes[0]
|
const shape = this.editor.selectedShapes[0]
|
||||||
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
||||||
this.editor.setCurrentTool('select')
|
this.editor.setCurrentTool('select')
|
||||||
this.editor.setEditingId(shape.id)
|
this.editor.setEditingShapeId(shape.id)
|
||||||
this.editor.root.current.value!.transition('editing_shape', {
|
this.editor.root.current.value!.transition('editing_shape', {
|
||||||
...info,
|
...info,
|
||||||
target: 'shape',
|
target: 'shape',
|
||||||
|
|
|
@ -8,7 +8,7 @@ export class Pointing extends StateNode {
|
||||||
markId = ''
|
markId = ''
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||||
|
@ -19,9 +19,10 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
|
|
||||||
this.markId = this.editor.mark(`creating:${id}`)
|
this.markId = `creating:${id}`
|
||||||
|
this.editor
|
||||||
this.editor.createShapes<TLTextShape>([
|
.mark(this.markId)
|
||||||
|
.createShapes<TLTextShape>([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
@ -34,8 +35,7 @@ export class Pointing extends StateNode {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
.select(id)
|
||||||
this.editor.select(id)
|
|
||||||
|
|
||||||
this.shape = this.editor.getShape(id)
|
this.shape = this.editor.getShape(id)
|
||||||
if (!this.shape) return
|
if (!this.shape) return
|
||||||
|
@ -72,8 +72,8 @@ export class Pointing extends StateNode {
|
||||||
this.editor.mark('creating text shape')
|
this.editor.mark('creating text shape')
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
const { x, y } = this.editor.inputs.currentPagePoint
|
const { x, y } = this.editor.inputs.currentPagePoint
|
||||||
this.editor.createShapes(
|
this.editor
|
||||||
[
|
.createShapes([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
@ -84,11 +84,10 @@ export class Pointing extends StateNode {
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
])
|
||||||
true
|
.select(id)
|
||||||
)
|
|
||||||
|
|
||||||
this.editor.setEditingId(id)
|
this.editor.setEditingShapeId(id)
|
||||||
this.editor.setCurrentTool('select')
|
this.editor.setCurrentTool('select')
|
||||||
this.editor.root.current.value?.transition('editing_shape', {})
|
this.editor.root.current.value?.transition('editing_shape', {})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,9 @@ export class EraserTool extends StateNode {
|
||||||
static override children = () => [Idle, Pointing, Erasing]
|
static override children = () => [Idle, Pointing, Erasing]
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,14 @@ export class Erasing extends StateNode {
|
||||||
private excludedShapeIds = new Set<TLShapeId>()
|
private excludedShapeIds = new Set<TLShapeId>()
|
||||||
|
|
||||||
override onEnter = (info: TLPointerEventInfo) => {
|
override onEnter = (info: TLPointerEventInfo) => {
|
||||||
this.markId = this.editor.mark('erase scribble begin')
|
|
||||||
this.info = info
|
this.info = info
|
||||||
|
|
||||||
|
this.markId = 'erase scribble begin'
|
||||||
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
const { originPagePoint } = this.editor.inputs
|
const { originPagePoint } = this.editor.inputs
|
||||||
this.excludedShapeIds = new Set(
|
this.excludedShapeIds = new Set(
|
||||||
this.editor.shapesOnCurrentPage
|
this.editor.currentPageShapes
|
||||||
.filter(
|
.filter(
|
||||||
(shape) =>
|
(shape) =>
|
||||||
this.editor.isShapeOrAncestorLocked(shape) ||
|
this.editor.isShapeOrAncestorLocked(shape) ||
|
||||||
|
@ -95,8 +97,8 @@ export class Erasing extends StateNode {
|
||||||
update() {
|
update() {
|
||||||
const {
|
const {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
shapesOnCurrentPage,
|
currentPageShapes,
|
||||||
erasingShapeIdsSet,
|
erasingShapeIds,
|
||||||
inputs: { currentPagePoint, previousPagePoint },
|
inputs: { currentPagePoint, previousPagePoint },
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
|
@ -104,9 +106,9 @@ export class Erasing extends StateNode {
|
||||||
|
|
||||||
this.pushPointToScribble()
|
this.pushPointToScribble()
|
||||||
|
|
||||||
const erasing = new Set<TLShapeId>(erasingShapeIdsSet)
|
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
||||||
|
|
||||||
for (const shape of shapesOnCurrentPage) {
|
for (const shape of currentPageShapes) {
|
||||||
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
||||||
|
|
||||||
// Avoid testing masked shapes, unless the pointer is inside the mask
|
// Avoid testing masked shapes, unless the pointer is inside the mask
|
||||||
|
@ -128,17 +130,17 @@ export class Erasing extends StateNode {
|
||||||
// Remove the hit shapes, except if they're in the list of excluded shapes
|
// Remove the hit shapes, except if they're in the list of excluded shapes
|
||||||
// (these excluded shapes will be any frames or groups the pointer was inside of
|
// (these excluded shapes will be any frames or groups the pointer was inside of
|
||||||
// when the user started erasing)
|
// when the user started erasing)
|
||||||
this.editor.setErasingIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
this.editor.setErasingShapeIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
complete() {
|
complete() {
|
||||||
this.editor.deleteShapes(this.editor.currentPageState.erasingShapeIds)
|
this.editor.deleteShapes(this.editor.currentPageState.erasingShapeIds)
|
||||||
this.editor.setErasingIds([])
|
this.editor.setErasingShapeIds([])
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.editor.setErasingIds([])
|
this.editor.setErasingShapeIds([])
|
||||||
this.editor.bailToMark(this.markId)
|
this.editor.bailToMark(this.markId)
|
||||||
this.parent.transition('idle', this.info)
|
this.parent.transition('idle', this.info)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export class Pointing extends StateNode {
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
const {
|
const {
|
||||||
inputs: { currentPagePoint },
|
inputs: { currentPagePoint },
|
||||||
sortedShapesOnCurrentPage,
|
currentPageShapesSorted,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
const initialSize = erasing.size
|
const initialSize = erasing.size
|
||||||
|
|
||||||
for (let n = sortedShapesOnCurrentPage.length, i = n - 1; i >= 0; i--) {
|
for (let n = currentPageShapesSorted.length, i = n - 1; i >= 0; i--) {
|
||||||
const shape = sortedShapesOnCurrentPage[i]
|
const shape = currentPageShapesSorted[i]
|
||||||
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export class Pointing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.setErasingIds([...erasing])
|
this.editor.setErasingShapeIds([...erasing])
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||||
|
@ -79,12 +79,12 @@ export class Pointing extends StateNode {
|
||||||
this.editor.deleteShapes(erasingShapeIds)
|
this.editor.deleteShapes(erasingShapeIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.setErasingIds([])
|
this.editor.setErasingShapeIds([])
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.editor.setErasingIds([])
|
this.editor.setErasingShapeIds([])
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,10 @@ export class Idle extends StateNode {
|
||||||
static override id = 'idle'
|
static override id = 'idle'
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'grab', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'grab', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||||
|
|
|
@ -5,7 +5,10 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.stopCameraAnimation()
|
this.editor.stopCameraAnimation()
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'grabbing', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||||
|
|
|
@ -9,6 +9,9 @@ export class LaserTool extends StateNode {
|
||||||
static override children = () => [Idle, Lasering]
|
static override children = () => [Idle, Lasering]
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,11 +72,11 @@ export class DragAndDropManager {
|
||||||
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
||||||
|
|
||||||
if (res && res.shouldHint) {
|
if (res && res.shouldHint) {
|
||||||
this.editor.setHintingIds([nextDroppingShape.id])
|
this.editor.setHintingShapeIds([nextDroppingShape.id])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we're dropping onto the page, then clear hinting ids
|
// If we're dropping onto the page, then clear hinting ids
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
}
|
}
|
||||||
|
|
||||||
cb?.()
|
cb?.()
|
||||||
|
@ -103,7 +103,7 @@ export class DragAndDropManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.droppingNodeTimer = null
|
this.droppingNodeTimer = null
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
|
|
|
@ -43,7 +43,7 @@ export class SelectTool extends StateNode {
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
if (this.editor.currentPageState.editingShapeId) {
|
if (this.editor.currentPageState.editingShapeId) {
|
||||||
this.editor.setEditingId(null)
|
this.editor.setEditingShapeId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class Brushing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.excludedShapeIds = new Set(
|
this.excludedShapeIds = new Set(
|
||||||
this.editor.shapesOnCurrentPage
|
this.editor.currentPageShapes
|
||||||
.filter(
|
.filter(
|
||||||
(shape) =>
|
(shape) =>
|
||||||
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||||
|
@ -96,7 +96,7 @@ export class Brushing extends StateNode {
|
||||||
const {
|
const {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
shapesOnCurrentPage,
|
currentPageShapes,
|
||||||
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
|
@ -118,8 +118,8 @@ export class Brushing extends StateNode {
|
||||||
|
|
||||||
const { excludedShapeIds } = this
|
const { excludedShapeIds } = this
|
||||||
|
|
||||||
testAllShapes: for (let i = 0, n = shapesOnCurrentPage.length; i < n; i++) {
|
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
||||||
shape = shapesOnCurrentPage[i]
|
shape = currentPageShapes[i]
|
||||||
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
||||||
if (results.has(shape.id)) continue testAllShapes
|
if (results.has(shape.id)) continue testAllShapes
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@ export class Idle extends StateNode {
|
||||||
static override id = 'idle'
|
static override id = 'idle'
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
|
|
||||||
const { onlySelectedShape } = this.editor
|
const { onlySelectedShape } = this.editor
|
||||||
|
|
||||||
|
@ -14,21 +17,22 @@ export class Idle extends StateNode {
|
||||||
// (which clears the cropping id) but still remain in this state.
|
// (which clears the cropping id) but still remain in this state.
|
||||||
this.editor.on('change-history', this.cleanupCroppingState)
|
this.editor.on('change-history', this.cleanupCroppingState)
|
||||||
|
|
||||||
this.editor.mark('crop')
|
|
||||||
|
|
||||||
if (onlySelectedShape) {
|
if (onlySelectedShape) {
|
||||||
this.editor.setCroppingId(onlySelectedShape.id)
|
this.editor.setCroppingShapeId(onlySelectedShape.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit: TLExitEventHandler = () => {
|
override onExit: TLExitEventHandler = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
|
|
||||||
this.editor.off('change-history', this.cleanupCroppingState)
|
this.editor.off('change-history', this.cleanupCroppingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.editor.setCurrentTool('select.idle', {})
|
this.editor.setCurrentTool('select.idle', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +40,7 @@ export class Idle extends StateNode {
|
||||||
if (this.editor.isMenuOpen) return
|
if (this.editor.isMenuOpen) return
|
||||||
|
|
||||||
if (info.ctrlKey) {
|
if (info.ctrlKey) {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.editor.setCurrentTool('select.brushing', info)
|
this.editor.setCurrentTool('select.brushing', info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -66,7 +70,7 @@ export class Idle extends StateNode {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
if (this.editor.getShapeUtil(info.shape)?.canCrop(info.shape)) {
|
if (this.editor.getShapeUtil(info.shape)?.canCrop(info.shape)) {
|
||||||
this.editor.setCroppingId(info.shape.id)
|
this.editor.setCroppingShapeId(info.shape.id)
|
||||||
this.editor.setSelectedShapeIds([info.shape.id])
|
this.editor.setSelectedShapeIds([info.shape.id])
|
||||||
this.editor.setCurrentTool('select.crop.pointing_crop', info)
|
this.editor.setCurrentTool('select.crop.pointing_crop', info)
|
||||||
} else {
|
} else {
|
||||||
|
@ -145,7 +149,7 @@ export class Idle extends StateNode {
|
||||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||||
switch (info.code) {
|
switch (info.code) {
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.editor.setCurrentTool('select.idle', {})
|
this.editor.setCurrentTool('select.idle', {})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -153,7 +157,7 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancel() {
|
private cancel() {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.editor.setCurrentTool('select.idle', {})
|
this.editor.setCurrentTool('select.idle', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,14 +25,20 @@ export class TranslatingCrop extends StateNode {
|
||||||
) => {
|
) => {
|
||||||
this.info = info
|
this.info = info
|
||||||
this.snapshot = this.createSnapshot()
|
this.snapshot = this.createSnapshot()
|
||||||
|
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
|
|
||||||
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'move', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.updateShapes()
|
this.updateShapes()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove = () => {
|
override onPointerMove = () => {
|
||||||
|
@ -99,7 +105,7 @@ export class TranslatingCrop extends StateNode {
|
||||||
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
||||||
|
|
||||||
if (partial) {
|
if (partial) {
|
||||||
this.editor.updateShapes([partial], true)
|
this.editor.updateShapes([partial], { squashing: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ export class Cropping extends StateNode {
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
this.info = info
|
this.info = info
|
||||||
this.markId = this.editor.mark('cropping')
|
this.markId = 'cropping'
|
||||||
|
this.editor.mark(this.markId)
|
||||||
this.snapshot = this.createSnapshot()
|
this.snapshot = this.createSnapshot()
|
||||||
this.updateShapes()
|
this.updateShapes()
|
||||||
}
|
}
|
||||||
|
@ -199,7 +200,7 @@ export class Cropping extends StateNode {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShapes([partial], true)
|
this.editor.updateShapes([partial], { squashing: true })
|
||||||
this.updateCursor()
|
this.updateCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +208,7 @@ export class Cropping extends StateNode {
|
||||||
if (this.info.onInteractionEnd) {
|
if (this.info.onInteractionEnd) {
|
||||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||||
} else {
|
} else {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +218,7 @@ export class Cropping extends StateNode {
|
||||||
if (this.info.onInteractionEnd) {
|
if (this.info.onInteractionEnd) {
|
||||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||||
} else {
|
} else {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,8 @@ export class DraggingHandle extends StateNode {
|
||||||
this.info = info
|
this.info = info
|
||||||
this.parent.currentToolIdMask = info.onInteractionEnd
|
this.parent.currentToolIdMask = info.onInteractionEnd
|
||||||
this.shapeId = shape.id
|
this.shapeId = shape.id
|
||||||
this.markId = isCreating ? `creating:${shape.id}` : this.editor.mark('dragging handle')
|
this.markId = isCreating ? `creating:${shape.id}` : 'dragging handle'
|
||||||
|
if (!isCreating) this.editor.mark(this.markId)
|
||||||
this.initialHandle = deepCopy(handle)
|
this.initialHandle = deepCopy(handle)
|
||||||
this.initialPageTransform = this.editor.getPageTransform(shape)!
|
this.initialPageTransform = this.editor.getPageTransform(shape)!
|
||||||
this.initialPageRotation = this.initialPageTransform.rotation()
|
this.initialPageRotation = this.initialPageTransform.rotation()
|
||||||
|
@ -60,7 +61,7 @@ export class DraggingHandle extends StateNode {
|
||||||
|
|
||||||
this.editor.updateInstanceState(
|
this.editor.updateInstanceState(
|
||||||
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
|
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
|
||||||
true
|
{ ephemeral: true, squashing: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// <!-- Only relevant to arrows
|
// <!-- Only relevant to arrows
|
||||||
|
@ -95,7 +96,7 @@ export class DraggingHandle extends StateNode {
|
||||||
this.isPrecise = false
|
this.isPrecise = false
|
||||||
|
|
||||||
if (initialTerminal?.type === 'binding') {
|
if (initialTerminal?.type === 'binding') {
|
||||||
this.editor.setHintingIds([initialTerminal.boundShapeId])
|
this.editor.setHintingShapeIds([initialTerminal.boundShapeId])
|
||||||
|
|
||||||
this.isPrecise = !Vec2d.Equals(initialTerminal.normalizedAnchor, { x: 0.5, y: 0.5 })
|
this.isPrecise = !Vec2d.Equals(initialTerminal.normalizedAnchor, { x: 0.5, y: 0.5 })
|
||||||
if (this.isPrecise) {
|
if (this.isPrecise) {
|
||||||
|
@ -104,7 +105,7 @@ export class DraggingHandle extends StateNode {
|
||||||
this.resetExactTimeout()
|
this.resetExactTimeout()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
}
|
}
|
||||||
// -->
|
// -->
|
||||||
|
|
||||||
|
@ -166,9 +167,12 @@ export class DraggingHandle extends StateNode {
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
this.editor.snaps.clear()
|
this.editor.snaps.clear()
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private complete() {
|
private complete() {
|
||||||
|
@ -280,7 +284,7 @@ export class DraggingHandle extends StateNode {
|
||||||
|
|
||||||
if (bindingAfter?.type === 'binding') {
|
if (bindingAfter?.type === 'binding') {
|
||||||
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
|
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
|
||||||
editor.setHintingIds([bindingAfter.boundShapeId])
|
editor.setHintingShapeIds([bindingAfter.boundShapeId])
|
||||||
this.pointingId = bindingAfter.boundShapeId
|
this.pointingId = bindingAfter.boundShapeId
|
||||||
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
|
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
|
||||||
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
|
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
|
||||||
|
@ -288,7 +292,7 @@ export class DraggingHandle extends StateNode {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hintingShapeIds.length > 0) {
|
if (hintingShapeIds.length > 0) {
|
||||||
editor.setHintingIds([])
|
editor.setHintingShapeIds([])
|
||||||
this.pointingId = null
|
this.pointingId = null
|
||||||
this.isPrecise = false
|
this.isPrecise = false
|
||||||
this.isPreciseId = null
|
this.isPreciseId = null
|
||||||
|
@ -298,7 +302,7 @@ export class DraggingHandle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes) {
|
if (changes) {
|
||||||
editor.updateShapes([next], true)
|
editor.updateShapes([next], { squashing: false, ephemeral: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class EditingShape extends StateNode {
|
||||||
if (!editingShapeId) return
|
if (!editingShapeId) return
|
||||||
|
|
||||||
// Clear the editing shape
|
// Clear the editing shape
|
||||||
this.editor.setEditingId(null)
|
this.editor.setEditingShapeId(null)
|
||||||
|
|
||||||
const shape = this.editor.getShape(editingShapeId)!
|
const shape = this.editor.getShape(editingShapeId)!
|
||||||
const util = this.editor.getShapeUtil(shape)
|
const util = this.editor.getShapeUtil(shape)
|
||||||
|
|
|
@ -23,7 +23,10 @@ export class Idle extends StateNode {
|
||||||
|
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||||
|
@ -438,7 +441,7 @@ export class Idle extends StateNode {
|
||||||
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
||||||
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||||
this.editor.mark('editing shape')
|
this.editor.mark('editing shape')
|
||||||
this.editor.setEditingId(shape.id)
|
this.editor.setEditingShapeId(shape.id)
|
||||||
this.parent.transition('editing_shape', info)
|
this.parent.transition('editing_shape', info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,7 +470,7 @@ export class Idle extends StateNode {
|
||||||
const shape = this.editor.getShape(id)
|
const shape = this.editor.getShape(id)
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
this.editor.setEditingId(id)
|
this.editor.setEditingShapeId(id)
|
||||||
this.editor.select(id)
|
this.editor.select(id)
|
||||||
this.parent.transition('editing_shape', info)
|
this.parent.transition('editing_shape', info)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,11 +29,14 @@ export class PointingCropHandle extends StateNode {
|
||||||
if (!selectedShape) return
|
if (!selectedShape) return
|
||||||
|
|
||||||
this.updateCursor(selectedShape)
|
this.updateCursor(selectedShape)
|
||||||
this.editor.setCroppingId(selectedShape.id)
|
this.editor.setCroppingShapeId(selectedShape.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +55,7 @@ export class PointingCropHandle extends StateNode {
|
||||||
if (this.info.onInteractionEnd) {
|
if (this.info.onInteractionEnd) {
|
||||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||||
} else {
|
} else {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +76,7 @@ export class PointingCropHandle extends StateNode {
|
||||||
if (this.info.onInteractionEnd) {
|
if (this.info.onInteractionEnd) {
|
||||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||||
} else {
|
} else {
|
||||||
this.editor.setCroppingId(null)
|
this.editor.setCroppingShapeId(null)
|
||||||
this.parent.transition('idle', {})
|
this.parent.transition('idle', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,21 @@ export class PointingHandle extends StateNode {
|
||||||
const initialTerminal = (info.shape as TLArrowShape).props[info.handle.id as 'start' | 'end']
|
const initialTerminal = (info.shape as TLArrowShape).props[info.handle.id as 'start' | 'end']
|
||||||
|
|
||||||
if (initialTerminal?.type === 'binding') {
|
if (initialTerminal?.type === 'binding') {
|
||||||
this.editor.setHintingIds([initialTerminal.boundShapeId])
|
this.editor.setHintingShapeIds([initialTerminal.boundShapeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'grabbing', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.editor.setHintingIds([])
|
this.editor.setHintingShapeIds([])
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||||
|
|
|
@ -28,7 +28,10 @@ export class PointingRotateHandle extends StateNode {
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPointerMove = () => {
|
override onPointerMove = () => {
|
||||||
|
|
|
@ -56,13 +56,16 @@ export class Resizing extends StateNode {
|
||||||
this.creationCursorOffset = creationCursorOffset
|
this.creationCursorOffset = creationCursorOffset
|
||||||
|
|
||||||
if (info.isCreating) {
|
if (info.isCreating) {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'cross', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.snapshot = this._createSnapshot()
|
this.snapshot = this._createSnapshot()
|
||||||
this.markId = isCreating
|
|
||||||
? `creating:${this.editor.onlySelectedShape!.id}`
|
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'starting resizing'
|
||||||
: this.editor.mark('starting resizing')
|
if (!isCreating) this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.handleResizeStart()
|
this.handleResizeStart()
|
||||||
this.updateShapes()
|
this.updateShapes()
|
||||||
|
@ -105,7 +108,7 @@ export class Resizing extends StateNode {
|
||||||
this.handleResizeEnd()
|
this.handleResizeEnd()
|
||||||
|
|
||||||
if (this.editAfterComplete && this.editor.onlySelectedShape) {
|
if (this.editAfterComplete && this.editor.onlySelectedShape) {
|
||||||
this.editor.setEditingId(this.editor.onlySelectedShape.id)
|
this.editor.setEditingShapeId(this.editor.onlySelectedShape.id)
|
||||||
this.editor.setCurrentTool('select')
|
this.editor.setCurrentTool('select')
|
||||||
this.editor.root.current.value!.transition('editing_shape', {})
|
this.editor.root.current.value!.transition('editing_shape', {})
|
||||||
return
|
return
|
||||||
|
@ -349,12 +352,15 @@ export class Resizing extends StateNode {
|
||||||
|
|
||||||
nextCursor.rotation = rotation
|
nextCursor.rotation = rotation
|
||||||
|
|
||||||
this.editor.updateInstanceState({ cursor: nextCursor })
|
this.editor.updateInstanceState({ cursor: nextCursor }, { ephemeral: true, squashing: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.editor.snaps.clear()
|
this.editor.snaps.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,8 @@ export class Rotating extends StateNode {
|
||||||
this.info = info
|
this.info = info
|
||||||
this.parent.currentToolIdMask = info.onInteractionEnd
|
this.parent.currentToolIdMask = info.onInteractionEnd
|
||||||
|
|
||||||
this.markId = this.editor.mark('rotate start')
|
this.markId = 'rotate start'
|
||||||
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
const snapshot = getRotationSnapshot({ editor: this.editor })
|
const snapshot = getRotationSnapshot({ editor: this.editor })
|
||||||
if (!snapshot) return this.parent.transition('idle', this.info)
|
if (!snapshot) return this.parent.transition('idle', this.info)
|
||||||
|
@ -40,7 +41,10 @@ export class Rotating extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'none', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'none', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.parent.currentToolIdMask = undefined
|
this.parent.currentToolIdMask = undefined
|
||||||
|
|
||||||
this.snapshot = {} as TLRotationSnapshot
|
this.snapshot = {} as TLRotationSnapshot
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class ScribbleBrushing extends StateNode {
|
||||||
private updateScribbleSelection(addPoint: boolean) {
|
private updateScribbleSelection(addPoint: boolean) {
|
||||||
const {
|
const {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
shapesOnCurrentPage,
|
currentPageShapes,
|
||||||
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ export class ScribbleBrushing extends StateNode {
|
||||||
this.pushPointToScribble()
|
this.pushPointToScribble()
|
||||||
}
|
}
|
||||||
|
|
||||||
const shapes = shapesOnCurrentPage
|
const shapes = currentPageShapes
|
||||||
let shape: TLShape, geometry: Geometry2d, A: Vec2d, B: Vec2d
|
let shape: TLShape, geometry: Geometry2d, A: Vec2d, B: Vec2d
|
||||||
|
|
||||||
for (let i = 0, n = shapes.length; i < n; i++) {
|
for (let i = 0, n = shapes.length; i < n; i++) {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class Translating extends StateNode {
|
||||||
snapshot: TranslatingSnapshot = {} as any
|
snapshot: TranslatingSnapshot = {} as any
|
||||||
|
|
||||||
markId = ''
|
markId = ''
|
||||||
|
initialMarkId = ''
|
||||||
|
|
||||||
isCloning = false
|
isCloning = false
|
||||||
isCreating = false
|
isCreating = false
|
||||||
|
@ -53,9 +54,12 @@ export class Translating extends StateNode {
|
||||||
this.isCreating = isCreating
|
this.isCreating = isCreating
|
||||||
this.editAfterComplete = editAfterComplete
|
this.editAfterComplete = editAfterComplete
|
||||||
|
|
||||||
this.markId = isCreating
|
this.initialMarkId = isCreating
|
||||||
? this.editor.mark(`creating:${this.editor.onlySelectedShape!.id}`)
|
? `creating:${this.editor.onlySelectedShape!.id}`
|
||||||
: this.editor.mark('translating')
|
: 'translating'
|
||||||
|
this.markId = this.initialMarkId
|
||||||
|
if (!isCreating) this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.handleEnter(info)
|
this.handleEnter(info)
|
||||||
this.editor.on('tick', this.updateParent)
|
this.editor.on('tick', this.updateParent)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +70,10 @@ export class Translating extends StateNode {
|
||||||
this.selectionSnapshot = {} as any
|
this.selectionSnapshot = {} as any
|
||||||
this.snapshot = {} as any
|
this.snapshot = {} as any
|
||||||
this.editor.snaps.clear()
|
this.editor.snaps.clear()
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'default', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.dragAndDropManager.clear()
|
this.dragAndDropManager.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +118,9 @@ export class Translating extends StateNode {
|
||||||
|
|
||||||
this.isCloning = true
|
this.isCloning = true
|
||||||
this.reset()
|
this.reset()
|
||||||
this.markId = this.editor.mark('translating')
|
|
||||||
|
this.markId = 'cloning'
|
||||||
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.editor.duplicateShapes(Array.from(this.editor.selectedShapeIds))
|
this.editor.duplicateShapes(Array.from(this.editor.selectedShapeIds))
|
||||||
|
|
||||||
|
@ -124,7 +133,10 @@ export class Translating extends StateNode {
|
||||||
this.isCloning = false
|
this.isCloning = false
|
||||||
this.snapshot = this.selectionSnapshot
|
this.snapshot = this.selectionSnapshot
|
||||||
this.reset()
|
this.reset()
|
||||||
this.markId = this.editor.mark('translating')
|
|
||||||
|
this.markId = this.initialMarkId
|
||||||
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.updateShapes()
|
this.updateShapes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +160,7 @@ export class Translating extends StateNode {
|
||||||
if (this.editAfterComplete) {
|
if (this.editAfterComplete) {
|
||||||
const onlySelected = this.editor.onlySelectedShape
|
const onlySelected = this.editor.onlySelectedShape
|
||||||
if (onlySelected) {
|
if (onlySelected) {
|
||||||
this.editor.setEditingId(onlySelected.id)
|
this.editor.setEditingShapeId(onlySelected.id)
|
||||||
this.editor.setCurrentTool('select')
|
this.editor.setCurrentTool('select')
|
||||||
this.editor.root.current.value!.transition('editing_shape', {})
|
this.editor.root.current.value!.transition('editing_shape', {})
|
||||||
}
|
}
|
||||||
|
@ -171,7 +183,10 @@ export class Translating extends StateNode {
|
||||||
this.isCloning = false
|
this.isCloning = false
|
||||||
this.info = info
|
this.info = info
|
||||||
|
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'move', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
this.selectionSnapshot = getTranslatingSnapshot(this.editor)
|
this.selectionSnapshot = getTranslatingSnapshot(this.editor)
|
||||||
|
|
||||||
// Don't clone on create; otherwise clone on altKey
|
// Don't clone on create; otherwise clone on altKey
|
||||||
|
@ -201,7 +216,7 @@ export class Translating extends StateNode {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
this.editor.updateShapes(changes)
|
this.editor.updateShapes(changes, { squashing: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +421,6 @@ export function moveShapesToPoint({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
true
|
{ squashing: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class ZoomTool extends StateNode {
|
||||||
this.currentToolIdMask = undefined
|
this.currentToolIdMask = undefined
|
||||||
this.editor.updateInstanceState(
|
this.editor.updateInstanceState(
|
||||||
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
|
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
|
||||||
true
|
{ ephemeral: true, squashing: true }
|
||||||
)
|
)
|
||||||
this.currentToolIdMask = undefined
|
this.currentToolIdMask = undefined
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,15 @@ export class ZoomTool extends StateNode {
|
||||||
|
|
||||||
private updateCursor() {
|
private updateCursor() {
|
||||||
if (this.editor.inputs.altKey) {
|
if (this.editor.inputs.altKey) {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'zoom-out', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'zoom-out', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.editor.updateInstanceState({ cursor: { type: 'zoom-in', rotation: 0 } }, true)
|
this.editor.updateInstanceState(
|
||||||
|
{ cursor: { type: 'zoom-in', rotation: 0 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -7,7 +7,7 @@ export function updateHoveredId(editor: Editor) {
|
||||||
margin: HIT_TEST_MARGIN / editor.zoomLevel,
|
margin: HIT_TEST_MARGIN / editor.zoomLevel,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hitShape) return editor.setHoveredId(null)
|
if (!hitShape) return editor.setHoveredShapeId(null)
|
||||||
|
|
||||||
let shapeToHover: TLShape | undefined = undefined
|
let shapeToHover: TLShape | undefined = undefined
|
||||||
|
|
||||||
|
@ -26,5 +26,5 @@ export function updateHoveredId(editor: Editor) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor.setHoveredId(shapeToHover.id)
|
return editor.setHoveredShapeId(shapeToHover.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,7 @@ export function BackToContent() {
|
||||||
// viewport... so we also need to narrow down the list to only shapes that
|
// viewport... so we also need to narrow down the list to only shapes that
|
||||||
// are ALSO in the viewport.
|
// are ALSO in the viewport.
|
||||||
const visibleShapes = renderingShapes.filter((s) => s.isInViewport)
|
const visibleShapes = renderingShapes.filter((s) => s.isInViewport)
|
||||||
const showBackToContentNow =
|
const showBackToContentNow = visibleShapes.length === 0 && editor.currentPageShapes.length > 0
|
||||||
visibleShapes.length === 0 && editor.shapesOnCurrentPage.length > 0
|
|
||||||
|
|
||||||
if (showBackToContentPrev !== showBackToContentNow) {
|
if (showBackToContentPrev !== showBackToContentNow) {
|
||||||
setShowBackToContent(showBackToContentNow)
|
setShowBackToContent(showBackToContentNow)
|
||||||
|
|
|
@ -7,7 +7,7 @@ export const HTMLCanvas = track(function HTMLCanvas() {
|
||||||
const rCanvas = React.useRef<HTMLCanvasElement>(null)
|
const rCanvas = React.useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
const camera = editor.camera
|
const camera = editor.camera
|
||||||
const shapes = editor.shapesOnCurrentPage
|
const shapes = editor.currentPageShapes
|
||||||
if (rCanvas.current) {
|
if (rCanvas.current) {
|
||||||
const cvs = rCanvas.current
|
const cvs = rCanvas.current
|
||||||
const ctx = cvs.getContext('2d')!
|
const ctx = cvs.getContext('2d')!
|
||||||
|
|
|
@ -54,16 +54,15 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
||||||
|
|
||||||
const onDoubleClick = React.useCallback(
|
const onDoubleClick = React.useCallback(
|
||||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (!editor.shapeIdsOnCurrentPage.size) return
|
if (!editor.currentPageShapeIds.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]
|
||||||
)
|
)
|
||||||
|
@ -71,14 +70,13 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
||||||
const onPointerDown = React.useCallback(
|
const onPointerDown = React.useCallback(
|
||||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
setPointerCapture(e.currentTarget, e)
|
setPointerCapture(e.currentTarget, e)
|
||||||
if (!editor.shapeIdsOnCurrentPage.size) return
|
if (!editor.currentPageShapeIds.size) return
|
||||||
|
|
||||||
rPointing.current = true
|
rPointing.current = true
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
const _vpPageBounds = editor.viewportPageBounds
|
const _vpPageBounds = editor.viewportPageBounds
|
||||||
|
@ -89,7 +87,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,26 +96,21 @@ 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 = Vec2d.Sub(point, minimap.originPagePoint)
|
||||||
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',
|
||||||
|
@ -178,9 +171,10 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
||||||
useQuickReactor(
|
useQuickReactor(
|
||||||
'minimap render when pagebounds or collaborators changes',
|
'minimap render when pagebounds or collaborators changes',
|
||||||
() => {
|
() => {
|
||||||
const { shapeIdsOnCurrentPage, viewportPageBounds, commonBoundsOfAllShapesOnCurrentPage } =
|
const { currentPageShapeIds, viewportPageBounds, commonBoundsOfAllShapesOnCurrentPage } =
|
||||||
editor
|
editor
|
||||||
|
|
||||||
|
// deref
|
||||||
const _dpr = devicePixelRatio.value
|
const _dpr = devicePixelRatio.value
|
||||||
|
|
||||||
minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
|
minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
|
||||||
|
@ -193,7 +187,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
||||||
|
|
||||||
const allShapeBounds = [] as (Box2d & { id: TLShapeId })[]
|
const allShapeBounds = [] as (Box2d & { id: TLShapeId })[]
|
||||||
|
|
||||||
shapeIdsOnCurrentPage.forEach((id) => {
|
currentPageShapeIds.forEach((id) => {
|
||||||
let pageBounds = editor.getPageBounds(id) as Box2d & { id: TLShapeId }
|
let pageBounds = editor.getPageBounds(id) as Box2d & { id: TLShapeId }
|
||||||
if (!pageBounds) return
|
if (!pageBounds) return
|
||||||
|
|
||||||
|
@ -217,7 +211,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
||||||
minimap.collaborators = presences.value
|
minimap.collaborators = presences.value
|
||||||
minimap.render()
|
minimap.render()
|
||||||
},
|
},
|
||||||
[editor, minimap]
|
[editor, minimap, devicePixelRatio]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const ZoomMenu = track(function ZoomMenu() {
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
|
|
||||||
const zoom = editor.zoomLevel
|
const zoom = editor.zoomLevel
|
||||||
const hasShapes = editor.shapeIdsOnCurrentPage.size > 0
|
const hasShapes = editor.currentPageShapeIds.size > 0
|
||||||
const hasSelected = editor.selectedShapeIds.length > 0
|
const hasSelected = editor.selectedShapeIds.length > 0
|
||||||
const isZoomedTo100 = editor.zoomLevel === 1
|
const isZoomedTo100 = editor.zoomLevel === 1
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const PageItemInput = function PageItemInput({
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
editor.renamePage(id, value ? value : 'New Page', true)
|
editor.updatePage({ id, name: value ? value : 'New Page' }, true)
|
||||||
},
|
},
|
||||||
[editor, id]
|
[editor, id]
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,7 @@ export const PageItemInput = function PageItemInput({
|
||||||
const handleComplete = useCallback(
|
const handleComplete = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
editor.mark('rename page')
|
editor.mark('rename page')
|
||||||
editor.renamePage(id, value || 'New Page', false)
|
editor.updatePage({ id, name: value || 'New Page' }, false)
|
||||||
},
|
},
|
||||||
[editor, id]
|
[editor, id]
|
||||||
)
|
)
|
||||||
|
|
|
@ -326,7 +326,7 @@ export const PageMenu = function PageMenu() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const name = window.prompt('Rename page', page.name)
|
const name = window.prompt('Rename page', page.name)
|
||||||
if (name && name !== page.name) {
|
if (name && name !== page.name) {
|
||||||
editor.renamePage(page.id, name)
|
editor.updatePage({ id: page.id, name }, true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDoubleClick={toggleEditing}
|
onDoubleClick={toggleEditing}
|
||||||
|
@ -379,10 +379,10 @@ export const PageMenu = function PageMenu() {
|
||||||
item={page}
|
item={page}
|
||||||
listSize={pages.length}
|
listSize={pages.length}
|
||||||
onRename={() => {
|
onRename={() => {
|
||||||
if (editor.isIos) {
|
if (editor.environment.isIos) {
|
||||||
const name = window.prompt('Rename page', page.name)
|
const name = window.prompt('Rename page', page.name)
|
||||||
if (name && name !== page.name) {
|
if (name && name !== page.name) {
|
||||||
editor.renamePage(page.id, name)
|
editor.updatePage({ id: page.id, name }, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
SharedStyleMap,
|
SharedStyleMap,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
minBy,
|
minBy,
|
||||||
|
useComputed,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
@ -33,9 +34,7 @@ interface StylePanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectToolStyles = [DefaultColorStyle, DefaultDashStyle, DefaultFillStyle, DefaultSizeStyle]
|
const selectToolStyles = [DefaultColorStyle, DefaultDashStyle, DefaultFillStyle, DefaultSizeStyle]
|
||||||
function getRelevantStyles(
|
function getRelevantStyles(editor: Editor): ReadonlySharedStyleMap | null {
|
||||||
editor: Editor
|
|
||||||
): { styles: ReadonlySharedStyleMap; opacity: SharedStyle<number> } | null {
|
|
||||||
const styles = new SharedStyleMap(editor.sharedStyles)
|
const styles = new SharedStyleMap(editor.sharedStyles)
|
||||||
const hasShape = editor.selectedShapeIds.length > 0 || !!editor.root.current.value?.shapeType
|
const hasShape = editor.selectedShapeIds.length > 0 || !!editor.root.current.value?.shapeType
|
||||||
|
|
||||||
|
@ -46,14 +45,38 @@ function getRelevantStyles(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (styles.size === 0 && !hasShape) return null
|
if (styles.size === 0 && !hasShape) return null
|
||||||
return { styles, opacity: editor.sharedOpacity }
|
return styles
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
|
export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
|
const sharedOpacity = useComputed<SharedStyle<number>>(
|
||||||
|
'sharedOpacity',
|
||||||
|
() => {
|
||||||
|
if (editor.isIn('select') && editor.selectedShapeIds.length > 0) {
|
||||||
|
let opacity: number | null = null
|
||||||
|
for (const shape of editor.selectedShapes) {
|
||||||
|
if (opacity === null) {
|
||||||
|
opacity = shape.opacity
|
||||||
|
} else if (opacity !== shape.opacity) {
|
||||||
|
return { type: 'mixed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opacity !== null) {
|
||||||
|
return { type: 'shared', value: opacity }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'shared', value: editor.instanceState.opacityForNextShape }
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
|
||||||
const relevantStyles = useValue('getRelevantStyles', () => getRelevantStyles(editor), [editor])
|
const relevantStyles = useValue('getRelevantStyles', () => getRelevantStyles(editor), [editor])
|
||||||
|
const opacity = useValue('opacity', () => sharedOpacity.value, [sharedOpacity])
|
||||||
|
|
||||||
const handlePointerOut = useCallback(() => {
|
const handlePointerOut = useCallback(() => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
|
@ -63,7 +86,7 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
|
||||||
|
|
||||||
if (!relevantStyles) return null
|
if (!relevantStyles) return null
|
||||||
|
|
||||||
const { styles, opacity } = relevantStyles
|
const styles = relevantStyles
|
||||||
const geo = styles.get(GeoShapeGeoStyle)
|
const geo = styles.get(GeoShapeGeoStyle)
|
||||||
const arrowheadEnd = styles.get(ArrowShapeArrowheadEndStyle)
|
const arrowheadEnd = styles.get(ArrowShapeArrowheadEndStyle)
|
||||||
const arrowheadStart = styles.get(ArrowShapeArrowheadStartStyle)
|
const arrowheadStart = styles.get(ArrowShapeArrowheadStartStyle)
|
||||||
|
@ -95,8 +118,8 @@ function useStyleChangeCallback() {
|
||||||
|
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
return function <T>(style: StyleProp<T>, value: T, squashing: boolean) {
|
return function <T>(style: StyleProp<T>, value: T, squashing: boolean) {
|
||||||
editor.setStyle(style, value, squashing)
|
editor.setStyle(style, value, { ephemeral: false, squashing })
|
||||||
editor.updateInstanceState({ isChangingStyle: true })
|
editor.updateInstanceState({ isChangingStyle: true }, { ephemeral: true, squashing: true })
|
||||||
}
|
}
|
||||||
}, [editor])
|
}, [editor])
|
||||||
}
|
}
|
||||||
|
@ -118,8 +141,16 @@ function CommonStylePickerSet({
|
||||||
const handleOpacityValueChange = React.useCallback(
|
const handleOpacityValueChange = React.useCallback(
|
||||||
(value: number, ephemeral: boolean) => {
|
(value: number, ephemeral: boolean) => {
|
||||||
const item = tldrawSupportedOpacities[value]
|
const item = tldrawSupportedOpacities[value]
|
||||||
editor.setOpacity(item, ephemeral)
|
editor.batch(() => {
|
||||||
editor.updateInstanceState({ isChangingStyle: true })
|
editor.setOpacity(item, { ephemeral, squashing: true })
|
||||||
|
if (editor.isIn('select')) {
|
||||||
|
editor.updateShapes(
|
||||||
|
editor.selectedShapes.map((s) => ({ ...s, opacity: item })),
|
||||||
|
{ squashing: true, ephemeral: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
editor.updateInstanceState({ isChangingStyle: true }, { ephemeral, squashing: true })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
|
@ -704,7 +704,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
{
|
{
|
||||||
id: 'delete',
|
id: 'delete',
|
||||||
label: 'action.delete',
|
label: 'action.delete',
|
||||||
kbd: '⌫,del,backspace',
|
kbd: '⌫,del', // removed backspace; it was firing twice on mac
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
@ -838,10 +838,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
trackEvent('toggle-transparent', { source })
|
trackEvent('toggle-transparent', { source })
|
||||||
editor.updateInstanceState(
|
editor.updateInstanceState(
|
||||||
{
|
{ exportBackground: !editor.instanceState.exportBackground },
|
||||||
exportBackground: !editor.instanceState.exportBackground,
|
{ ephemeral: true, squashing: true }
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
|
@ -901,7 +899,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
{
|
{
|
||||||
isDebugMode: !editor.instanceState.isDebugMode,
|
isDebugMode: !editor.instanceState.isDebugMode,
|
||||||
},
|
},
|
||||||
true
|
{ ephemeral: true, squashing: true }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
|
|
|
@ -93,7 +93,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem
|
||||||
const threeStackableItems = useThreeStackableItems()
|
const threeStackableItems = useThreeStackableItems()
|
||||||
const atLeastOneShapeOnPage = useValue(
|
const atLeastOneShapeOnPage = useValue(
|
||||||
'atLeastOneShapeOnPage',
|
'atLeastOneShapeOnPage',
|
||||||
() => editor.shapeIdsOnCurrentPage.size > 0,
|
() => editor.currentPageShapeIds.size > 0,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const isTransparentBg = useValue(
|
const isTransparentBg = useValue(
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function useCopyAs() {
|
||||||
// little awkward.
|
// little awkward.
|
||||||
function copyAs(ids: TLShapeId[] = editor.selectedShapeIds, format: TLCopyType = 'svg') {
|
function copyAs(ids: TLShapeId[] = editor.selectedShapeIds, format: TLCopyType = 'svg') {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
ids = [...editor.shapeIdsOnCurrentPage]
|
ids = [...editor.currentPageShapeIds]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function useExportAs() {
|
||||||
format: TLExportType = 'png'
|
format: TLExportType = 'png'
|
||||||
) {
|
) {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
ids = [...editor.shapeIdsOnCurrentPage]
|
ids = [...editor.currentPageShapeIds]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { preventDefault, useEditor, useValue } from '@tldraw/editor'
|
import { preventDefault, uniqueId, useEditor, useValue } from '@tldraw/editor'
|
||||||
import hotkeys from 'hotkeys-js'
|
import hotkeys from 'hotkeys-js'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useActions } from './useActions'
|
import { useActions } from './useActions'
|
||||||
|
@ -26,12 +26,12 @@ export function useKeyboardShortcuts() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFocused) return
|
if (!isFocused) return
|
||||||
|
|
||||||
|
const scope = uniqueId()
|
||||||
|
|
||||||
const container = editor.getContainer()
|
const container = editor.getContainer()
|
||||||
|
|
||||||
hotkeys.setScope(editor.store.id)
|
|
||||||
|
|
||||||
const hot = (keys: string, callback: (event: KeyboardEvent) => void) => {
|
const hot = (keys: string, callback: (event: KeyboardEvent) => void) => {
|
||||||
hotkeys(keys, { element: container, scope: editor.store.id }, callback)
|
hotkeys(keys, { element: container, scope }, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add hotkeys for actions and tools.
|
// Add hotkeys for actions and tools.
|
||||||
|
@ -65,8 +65,11 @@ export function useKeyboardShortcuts() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When focused, hotkeys should only respond to kbds in this scope
|
||||||
|
hotkeys.setScope(scope)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
hotkeys.deleteScope(editor.store.id)
|
hotkeys.deleteScope(scope)
|
||||||
}
|
}
|
||||||
}, [actions, tools, isReadonly, editor, isFocused])
|
}, [actions, tools, isReadonly, editor, isFocused])
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
|
||||||
[editor]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const emptyPage = useValue('emptyPage', () => editor.shapeIdsOnCurrentPage.size === 0, [editor])
|
const emptyPage = useValue('emptyPage', () => editor.currentPageShapeIds.size === 0, [editor])
|
||||||
|
|
||||||
const selectedCount = useValue('selectedCount', () => editor.selectedShapeIds.length, [editor])
|
const selectedCount = useValue('selectedCount', () => editor.selectedShapeIds.length, [editor])
|
||||||
const noneSelected = selectedCount === 0
|
const noneSelected = selectedCount === 0
|
||||||
|
|
|
@ -156,10 +156,10 @@ export function usePrint() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerPrint() {
|
function triggerPrint() {
|
||||||
if (editor.isChromeForIos) {
|
if (editor.environment.isChromeForIos) {
|
||||||
beforePrintHandler()
|
beforePrintHandler()
|
||||||
window.print()
|
window.print()
|
||||||
} else if (editor.isSafari) {
|
} else if (editor.environment.isSafari) {
|
||||||
beforePrintHandler()
|
beforePrintHandler()
|
||||||
document.execCommand('print', false)
|
document.execCommand('print', false)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -111,7 +111,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
|
||||||
[GeoShapeGeoStyle.id]: id,
|
[GeoShapeGeoStyle.id]: id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true
|
{ ephemeral: true, squashing: true }
|
||||||
)
|
)
|
||||||
editor.setCurrentTool('geo')
|
editor.setCurrentTool('geo')
|
||||||
trackEvent('select-tool', { source, id: `geo-${id}` })
|
trackEvent('select-tool', { source, id: `geo-${id}` })
|
||||||
|
|
|
@ -178,7 +178,7 @@ export function useRegisterExternalContentHandlers() {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.createShapes([shapePartial], true)
|
editor.createShapes([shapePartial]).select(shapePartial.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// files
|
// files
|
||||||
|
@ -405,7 +405,7 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the shapes
|
// Create the shapes
|
||||||
editor.createShapes(paritals, true)
|
editor.createShapes(paritals).select(...paritals.map((p) => p.id))
|
||||||
|
|
||||||
// Re-position shapes so that the center of the group is at the provided point
|
// Re-position shapes so that the center of the group is at the provided point
|
||||||
const { viewportPageBounds } = editor
|
const { viewportPageBounds } = editor
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { BaseBoxShapeUtil, PageRecordType, TLShape, createShapeId } from '@tldraw/editor'
|
import { BaseBoxShapeUtil, PageRecordType, TLShape, createShapeId } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
import { TL } from './test-jsx'
|
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
|
||||||
|
@ -53,13 +52,13 @@ describe('shapes that are moved to another page', () => {
|
||||||
|
|
||||||
describe("should be excluded from the previous page's hintingShapeIds", () => {
|
describe("should be excluded from the previous page's hintingShapeIds", () => {
|
||||||
test('[boxes]', () => {
|
test('[boxes]', () => {
|
||||||
editor.setHintingIds([ids.box1, ids.box2, ids.box3])
|
editor.setHintingShapeIds([ids.box1, ids.box2, ids.box3])
|
||||||
expect(editor.hintingShapeIds).toEqual([ids.box1, ids.box2, ids.box3])
|
expect(editor.hintingShapeIds).toEqual([ids.box1, ids.box2, ids.box3])
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.hintingShapeIds).toEqual([])
|
expect(editor.hintingShapeIds).toEqual([])
|
||||||
})
|
})
|
||||||
test('[frame that does not move]', () => {
|
test('[frame that does not move]', () => {
|
||||||
editor.setHintingIds([ids.frame1])
|
editor.setHintingShapeIds([ids.frame1])
|
||||||
expect(editor.hintingShapeIds).toEqual([ids.frame1])
|
expect(editor.hintingShapeIds).toEqual([ids.frame1])
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.hintingShapeIds).toEqual([ids.frame1])
|
expect(editor.hintingShapeIds).toEqual([ids.frame1])
|
||||||
|
@ -68,25 +67,25 @@ describe('shapes that are moved to another page', () => {
|
||||||
|
|
||||||
describe("should be excluded from the previous page's editingShapeId", () => {
|
describe("should be excluded from the previous page's editingShapeId", () => {
|
||||||
test('[root shape]', () => {
|
test('[root shape]', () => {
|
||||||
editor.setEditingId(ids.box1)
|
editor.setEditingShapeId(ids.box1)
|
||||||
expect(editor.editingShapeId).toBe(ids.box1)
|
expect(editor.editingShapeId).toBe(ids.box1)
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
})
|
})
|
||||||
test('[child of frame]', () => {
|
test('[child of frame]', () => {
|
||||||
editor.setEditingId(ids.box2)
|
editor.setEditingShapeId(ids.box2)
|
||||||
expect(editor.editingShapeId).toBe(ids.box2)
|
expect(editor.editingShapeId).toBe(ids.box2)
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
})
|
})
|
||||||
test('[child of group]', () => {
|
test('[child of group]', () => {
|
||||||
editor.setEditingId(ids.box3)
|
editor.setEditingShapeId(ids.box3)
|
||||||
expect(editor.editingShapeId).toBe(ids.box3)
|
expect(editor.editingShapeId).toBe(ids.box3)
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
})
|
})
|
||||||
test('[frame that doesnt move]', () => {
|
test('[frame that doesnt move]', () => {
|
||||||
editor.setEditingId(ids.frame1)
|
editor.setEditingShapeId(ids.frame1)
|
||||||
expect(editor.editingShapeId).toBe(ids.frame1)
|
expect(editor.editingShapeId).toBe(ids.frame1)
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.editingShapeId).toBe(ids.frame1)
|
expect(editor.editingShapeId).toBe(ids.frame1)
|
||||||
|
@ -95,13 +94,13 @@ describe('shapes that are moved to another page', () => {
|
||||||
|
|
||||||
describe("should be excluded from the previous page's erasingShapeIds", () => {
|
describe("should be excluded from the previous page's erasingShapeIds", () => {
|
||||||
test('[boxes]', () => {
|
test('[boxes]', () => {
|
||||||
editor.setErasingIds([ids.box1, ids.box2, ids.box3])
|
editor.setErasingShapeIds([ids.box1, ids.box2, ids.box3])
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1, ids.box2, ids.box3])
|
expect(editor.erasingShapeIds).toEqual([ids.box1, ids.box2, ids.box3])
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
})
|
})
|
||||||
test('[frame that does not move]', () => {
|
test('[frame that does not move]', () => {
|
||||||
editor.setErasingIds([ids.frame1])
|
editor.setErasingShapeIds([ids.frame1])
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.frame1])
|
expect(editor.erasingShapeIds).toEqual([ids.frame1])
|
||||||
moveShapesToPage2()
|
moveShapesToPage2()
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.frame1])
|
expect(editor.erasingShapeIds).toEqual([ids.frame1])
|
||||||
|
@ -147,89 +146,89 @@ it('Does not create an undo stack item when first clicking on an empty canvas',
|
||||||
expect(editor.canUndo).toBe(false)
|
expect(editor.canUndo).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Editor.sharedOpacity', () => {
|
// describe('Editor.sharedOpacity', () => {
|
||||||
it('should return the current opacity', () => {
|
// it('should return the current opacity', () => {
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 1 })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 1 })
|
||||||
editor.setOpacity(0.5)
|
// editor.setOpacity(0.5)
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.5 })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.5 })
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('should return opacity for a single selected shape', () => {
|
// it('should return opacity for a single selected shape', () => {
|
||||||
const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
|
// const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
|
||||||
editor.setSelectedShapeIds([A])
|
// editor.setSelectedShapeIds([A])
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('should return opacity for multiple selected shapes', () => {
|
// it('should return opacity for multiple selected shapes', () => {
|
||||||
const { A, B } = editor.createShapesFromJsx([
|
// const { A, B } = editor.createShapesFromJsx([
|
||||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||||
<TL.geo ref="B" opacity={0.3} x={0} y={0} />,
|
// <TL.geo ref="B" opacity={0.3} x={0} y={0} />,
|
||||||
])
|
// ])
|
||||||
editor.setSelectedShapeIds([A, B])
|
// editor.setSelectedShapeIds([A, B])
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('should return mixed when multiple selected shapes have different opacity', () => {
|
// it('should return mixed when multiple selected shapes have different opacity', () => {
|
||||||
const { A, B } = editor.createShapesFromJsx([
|
// const { A, B } = editor.createShapesFromJsx([
|
||||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||||
<TL.geo ref="B" opacity={0.5} x={0} y={0} />,
|
// <TL.geo ref="B" opacity={0.5} x={0} y={0} />,
|
||||||
])
|
// ])
|
||||||
editor.setSelectedShapeIds([A, B])
|
// editor.setSelectedShapeIds([A, B])
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('ignores the opacity of groups and returns the opacity of their children', () => {
|
// it('ignores the opacity of groups and returns the opacity of their children', () => {
|
||||||
const ids = editor.createShapesFromJsx([
|
// const ids = editor.createShapesFromJsx([
|
||||||
<TL.group ref="group" x={0} y={0}>
|
// <TL.group ref="group" x={0} y={0}>
|
||||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />
|
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />
|
||||||
</TL.group>,
|
// </TL.group>,
|
||||||
])
|
// ])
|
||||||
editor.setSelectedShapeIds([ids.group])
|
// editor.setSelectedShapeIds([ids.group])
|
||||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
||||||
describe('Editor.setOpacity', () => {
|
describe('Editor.setOpacity', () => {
|
||||||
it('should set opacity for selected shapes', () => {
|
// it('should set opacity for selected shapes', () => {
|
||||||
const ids = editor.createShapesFromJsx([
|
// const ids = editor.createShapesFromJsx([
|
||||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||||
<TL.geo ref="B" opacity={0.4} x={0} y={0} />,
|
// <TL.geo ref="B" opacity={0.4} x={0} y={0} />,
|
||||||
])
|
// ])
|
||||||
|
|
||||||
editor.setSelectedShapeIds([ids.A, ids.B])
|
// editor.setSelectedShapeIds([ids.A, ids.B])
|
||||||
editor.setOpacity(0.5)
|
// editor.setOpacity(0.5)
|
||||||
|
|
||||||
expect(editor.getShape(ids.A)!.opacity).toBe(0.5)
|
// expect(editor.getShape(ids.A)!.opacity).toBe(0.5)
|
||||||
expect(editor.getShape(ids.B)!.opacity).toBe(0.5)
|
// expect(editor.getShape(ids.B)!.opacity).toBe(0.5)
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('should traverse into groups and set opacity in their children', () => {
|
// it('should traverse into groups and set opacity in their children', () => {
|
||||||
const ids = editor.createShapesFromJsx([
|
// const ids = editor.createShapesFromJsx([
|
||||||
<TL.geo ref="boxA" x={0} y={0} />,
|
// <TL.geo ref="boxA" x={0} y={0} />,
|
||||||
<TL.group ref="groupA" x={0} y={0}>
|
// <TL.group ref="groupA" x={0} y={0}>
|
||||||
<TL.geo ref="boxB" x={0} y={0} />
|
// <TL.geo ref="boxB" x={0} y={0} />
|
||||||
<TL.group ref="groupB" x={0} y={0}>
|
// <TL.group ref="groupB" x={0} y={0}>
|
||||||
<TL.geo ref="boxC" x={0} y={0} />
|
// <TL.geo ref="boxC" x={0} y={0} />
|
||||||
<TL.geo ref="boxD" x={0} y={0} />
|
// <TL.geo ref="boxD" x={0} y={0} />
|
||||||
</TL.group>
|
// </TL.group>
|
||||||
</TL.group>,
|
// </TL.group>,
|
||||||
])
|
// ])
|
||||||
|
|
||||||
editor.setSelectedShapeIds([ids.groupA])
|
// editor.setSelectedShapeIds([ids.groupA])
|
||||||
editor.setOpacity(0.5)
|
// editor.setOpacity(0.5)
|
||||||
|
|
||||||
// a wasn't selected...
|
// // a wasn't selected...
|
||||||
expect(editor.getShape(ids.boxA)!.opacity).toBe(1)
|
// expect(editor.getShape(ids.boxA)!.opacity).toBe(1)
|
||||||
|
|
||||||
// b, c, & d were within a selected group...
|
// // b, c, & d were within a selected group...
|
||||||
expect(editor.getShape(ids.boxB)!.opacity).toBe(0.5)
|
// expect(editor.getShape(ids.boxB)!.opacity).toBe(0.5)
|
||||||
expect(editor.getShape(ids.boxC)!.opacity).toBe(0.5)
|
// expect(editor.getShape(ids.boxC)!.opacity).toBe(0.5)
|
||||||
expect(editor.getShape(ids.boxD)!.opacity).toBe(0.5)
|
// expect(editor.getShape(ids.boxD)!.opacity).toBe(0.5)
|
||||||
|
|
||||||
// groups get skipped
|
// // groups get skipped
|
||||||
expect(editor.getShape(ids.groupA)!.opacity).toBe(1)
|
// expect(editor.getShape(ids.groupA)!.opacity).toBe(1)
|
||||||
expect(editor.getShape(ids.groupB)!.opacity).toBe(1)
|
// expect(editor.getShape(ids.groupB)!.opacity).toBe(1)
|
||||||
})
|
// })
|
||||||
|
|
||||||
it('stores opacity on opacityForNextShape', () => {
|
it('stores opacity on opacityForNextShape', () => {
|
||||||
editor.setOpacity(0.5)
|
editor.setOpacity(0.5)
|
||||||
|
|
|
@ -99,7 +99,7 @@ describe('When clicking', () => {
|
||||||
// Starts in idle
|
// Starts in idle
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(0, 0) // near enough to box1
|
editor.pointerDown(0, 0) // near enough to box1
|
||||||
|
|
||||||
|
@ -108,11 +108,10 @@ describe('When clicking', () => {
|
||||||
|
|
||||||
// Sets the erasingShapeIds array / erasingShapeIdsSet
|
// Sets the erasingShapeIds array / erasingShapeIdsSet
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
// Deletes the erasing shapes
|
// Deletes the erasing shapes
|
||||||
expect(editor.getShape(ids.box1)).toBeUndefined()
|
expect(editor.getShape(ids.box1)).toBeUndefined()
|
||||||
|
@ -120,7 +119,6 @@ describe('When clicking', () => {
|
||||||
|
|
||||||
// Also empties the erasingShapeIds array / erasingShapeIdsSet
|
// Also empties the erasingShapeIds array / erasingShapeIdsSet
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
|
|
||||||
// Returns to idle
|
// Returns to idle
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
@ -128,30 +126,29 @@ describe('When clicking', () => {
|
||||||
editor.undo()
|
editor.undo()
|
||||||
|
|
||||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(shapesBeforeCount)
|
expect(editor.currentPageShapes.length).toBe(shapesBeforeCount)
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.getShape(ids.box1)).toBeUndefined()
|
expect(editor.getShape(ids.box1)).toBeUndefined()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(shapesBeforeCount - 1)
|
expect(editor.currentPageShapes.length).toBe(shapesBeforeCount - 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Erases all shapes under the cursor on click', () => {
|
it('Erases all shapes under the cursor on click', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(99, 99) // next to box1 AND in box2
|
editor.pointerDown(99, 99) // next to box1 AND in box2
|
||||||
|
|
||||||
expect(new Set(editor.erasingShapeIds)).toEqual(new Set([ids.box1, ids.box2]))
|
expect(new Set(editor.erasingShapeIds)).toEqual(new Set([ids.box1, ids.box2]))
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1, ids.box2]))
|
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
expect(editor.getShape(ids.box1)).toBeUndefined()
|
expect(editor.getShape(ids.box1)).toBeUndefined()
|
||||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount - 2)
|
expect(shapesAfterCount).toBe(shapesBeforeCount - 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -159,16 +156,15 @@ describe('When clicking', () => {
|
||||||
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(350, 350) // in box3
|
editor.pointerDown(350, 350) // in box3
|
||||||
|
|
||||||
expect(new Set(editor.erasingShapeIds)).toEqual(new Set([ids.group1]))
|
expect(new Set(editor.erasingShapeIds)).toEqual(new Set([ids.group1]))
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.group1]))
|
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
expect(editor.getShape(ids.box3)).toBeUndefined()
|
expect(editor.getShape(ids.box3)).toBeUndefined()
|
||||||
|
@ -181,28 +177,26 @@ describe('When clicking', () => {
|
||||||
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(275, 275) // in between box2 AND box3, so over of the new group
|
editor.pointerDown(275, 275) // in between box2 AND box3, so over of the new group
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Stops erasing when it reaches a frame when the frame was not was the top-most hovered shape', () => {
|
it('Stops erasing when it reaches a frame when the frame was not was the top-most hovered shape', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(375, 75) // inside of the box4 shape inside of box3
|
editor.pointerDown(375, 75) // inside of the box4 shape inside of box3
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box4]))
|
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount - 1)
|
expect(shapesAfterCount).toBe(shapesBeforeCount - 1)
|
||||||
|
|
||||||
// Erases the child but does not erase the frame
|
// Erases the child but does not erase the frame
|
||||||
|
@ -213,14 +207,13 @@ describe('When clicking', () => {
|
||||||
it('Erases a frame only when its clicked on the edge', () => {
|
it('Erases a frame only when its clicked on the edge', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(325, 25) // directly on frame1, not its children
|
editor.pointerDown(325, 25) // directly on frame1, not its children
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
|
|
||||||
editor.pointerUp() // without dragging!
|
editor.pointerUp() // without dragging!
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||||
|
|
||||||
// Erases BOTH the frame and its child
|
// Erases BOTH the frame and its child
|
||||||
|
@ -231,14 +224,13 @@ describe('When clicking', () => {
|
||||||
it('Only erases masked shapes when pointer is inside the mask', () => {
|
it('Only erases masked shapes when pointer is inside the mask', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(425, 125) // inside of box4's bounds, but outside of its parent's mask
|
editor.pointerDown(425, 125) // inside of box4's bounds, but outside of its parent's mask
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
|
|
||||||
editor.pointerUp() // without dragging!
|
editor.pointerUp() // without dragging!
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||||
|
|
||||||
// Erases NEITHER the frame nor its child
|
// Erases NEITHER the frame nor its child
|
||||||
|
@ -250,25 +242,23 @@ describe('When clicking', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(0, 0) // in box1
|
editor.pointerDown(0, 0) // in box1
|
||||||
editor.expectPathToBe('root.eraser.pointing')
|
editor.expectPathToBe('root.eraser.pointing')
|
||||||
|
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
|
|
||||||
editor.cancel()
|
editor.cancel()
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
|
||||||
// Does NOT erase the shape
|
// Does NOT erase the shape
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||||
})
|
})
|
||||||
|
@ -277,25 +267,23 @@ describe('When clicking', () => {
|
||||||
editor.setCurrentTool('eraser')
|
editor.setCurrentTool('eraser')
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
|
||||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
const shapesBeforeCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.pointerDown(0, 0) // near to box1
|
editor.pointerDown(0, 0) // near to box1
|
||||||
editor.expectPathToBe('root.eraser.pointing')
|
editor.expectPathToBe('root.eraser.pointing')
|
||||||
|
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
|
|
||||||
editor.interrupt()
|
editor.interrupt()
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
|
||||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
const shapesAfterCount = editor.currentPageShapes.length
|
||||||
|
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
|
|
||||||
// Does NOT erase the shape
|
// Does NOT erase the shape
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||||
})
|
})
|
||||||
|
@ -320,24 +308,20 @@ describe('When clicking and dragging', () => {
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
|
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
|
|
||||||
// editor.pointerUp()
|
// editor.pointerUp()
|
||||||
// editor.expectPathToBe('root.eraser.idle')
|
// editor.expectPathToBe('root.eraser.idle')
|
||||||
// expect(editor.erasingShapeIds).toEqual([])
|
// expect(editor.erasingShapeIds).toEqual([])
|
||||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
||||||
|
|
||||||
// editor.undo()
|
// editor.undo()
|
||||||
|
|
||||||
// expect(editor.erasingShapeIds).toEqual([])
|
// expect(editor.erasingShapeIds).toEqual([])
|
||||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
// expect(editor.getShape(ids.box1)).toBeDefined()
|
// expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
|
|
||||||
// editor.redo()
|
// editor.redo()
|
||||||
|
|
||||||
// expect(editor.erasingShapeIds).toEqual([])
|
// expect(editor.erasingShapeIds).toEqual([])
|
||||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -349,11 +333,9 @@ describe('When clicking and dragging', () => {
|
||||||
jest.advanceTimersByTime(16)
|
jest.advanceTimersByTime(16)
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
editor.cancel()
|
editor.cancel()
|
||||||
editor.expectPathToBe('root.eraser.idle')
|
editor.expectPathToBe('root.eraser.idle')
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -366,10 +348,8 @@ describe('When clicking and dragging', () => {
|
||||||
jest.advanceTimersByTime(16)
|
jest.advanceTimersByTime(16)
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
editor.pointerMove(0, 0)
|
editor.pointerMove(0, 0)
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
|
||||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.getShape(ids.group1)).toBeDefined()
|
expect(editor.getShape(ids.group1)).toBeDefined()
|
||||||
|
@ -383,7 +363,6 @@ describe('When clicking and dragging', () => {
|
||||||
jest.advanceTimersByTime(16)
|
jest.advanceTimersByTime(16)
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box3]))
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.getShape(ids.frame1)).toBeDefined()
|
expect(editor.getShape(ids.frame1)).toBeDefined()
|
||||||
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
||||||
|
@ -397,7 +376,6 @@ describe('When clicking and dragging', () => {
|
||||||
jest.advanceTimersByTime(16)
|
jest.advanceTimersByTime(16)
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.getShape(ids.box3)).toBeDefined()
|
expect(editor.getShape(ids.box3)).toBeDefined()
|
||||||
|
|
||||||
|
@ -405,7 +383,6 @@ describe('When clicking and dragging', () => {
|
||||||
editor.pointerMove(375, 500) // Through the masked part of box3
|
editor.pointerMove(375, 500) // Through the masked part of box3
|
||||||
expect(editor.instanceState.scribble).not.toBe(null)
|
expect(editor.instanceState.scribble).not.toBe(null)
|
||||||
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
||||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box3]))
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
||||||
})
|
})
|
||||||
|
@ -437,7 +414,7 @@ describe('When clicking and dragging', () => {
|
||||||
describe('Does not erase hollow shapes on click', () => {
|
describe('Does not erase hollow shapes on click', () => {
|
||||||
it('Returns to select on cancel', () => {
|
it('Returns to select on cancel', () => {
|
||||||
editor.selectAll().deleteShapes(editor.selectedShapes)
|
editor.selectAll().deleteShapes(editor.selectedShapes)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.createShape({
|
editor.createShape({
|
||||||
id: createShapeId(),
|
id: createShapeId(),
|
||||||
type: 'geo',
|
type: 'geo',
|
||||||
|
@ -447,7 +424,7 @@ describe('Does not erase hollow shapes on click', () => {
|
||||||
editor.pointerDown()
|
editor.pointerDown()
|
||||||
expect(editor.erasingShapeIds).toEqual([])
|
expect(editor.erasingShapeIds).toEqual([])
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -45,33 +45,33 @@ describe('TLSelectTool.Translating', () => {
|
||||||
editor.pointerDown(150, 150, { target: 'shape', shape })
|
editor.pointerDown(150, 150, { target: 'shape', shape })
|
||||||
editor.pointerMove(200, 200)
|
editor.pointerMove(200, 200)
|
||||||
|
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
||||||
const t1 = [...editor.shapeIdsOnCurrentPage.values()]
|
const t1 = [...editor.currentPageShapeIds.values()]
|
||||||
|
|
||||||
editor.keyDown('Alt')
|
editor.keyDown('Alt')
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
||||||
// const t2 = [...editor.shapeIds.values()]
|
// const t2 = [...editor.shapeIds.values()]
|
||||||
|
|
||||||
editor.keyUp('Alt')
|
editor.keyUp('Alt')
|
||||||
|
|
||||||
// There's a timer here! We shouldn't end the clone until the timer is done
|
// There's a timer here! We shouldn't end the clone until the timer is done
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
|
|
||||||
jest.advanceTimersByTime(250) // tick tock
|
jest.advanceTimersByTime(250) // tick tock
|
||||||
|
|
||||||
// Timer is done! We should have ended the clone.
|
// Timer is done! We should have ended the clone.
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
editor.expectToBeIn('select.translating')
|
editor.expectToBeIn('select.translating')
|
||||||
|
|
||||||
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
||||||
|
|
||||||
expect([...editor.shapeIdsOnCurrentPage.values()]).toMatchObject(t1)
|
expect([...editor.currentPageShapeIds.values()]).toMatchObject(t1)
|
||||||
|
|
||||||
// todo: Should cloning again duplicate new shapes, or restore the last clone?
|
// todo: Should cloning again duplicate new shapes, or restore the last clone?
|
||||||
// editor.keyDown('Alt')
|
// editor.keyDown('Alt')
|
||||||
// expect(editor.shapesOnCurrentPage.length).toBe(2)
|
// expect(editor.currentPageShapes.length).toBe(2)
|
||||||
// editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
// editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
||||||
// expect([...editor.shapeIds.values()]).toMatchObject(t2)
|
// expect([...editor.shapeIds.values()]).toMatchObject(t2)
|
||||||
})
|
})
|
||||||
|
@ -95,7 +95,7 @@ describe('TLSelectTool.Translating', () => {
|
||||||
editor.pointerMove(150, 250)
|
editor.pointerMove(150, 250)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
const box2Id = editor.onlySelectedShape!.id
|
const box2Id = editor.onlySelectedShape!.id
|
||||||
expect(editor.shapesOnCurrentPage.length).toStrictEqual(2)
|
expect(editor.currentPageShapes.length).toStrictEqual(2)
|
||||||
expect(ids.box1).not.toEqual(box2Id)
|
expect(ids.box1).not.toEqual(box2Id)
|
||||||
|
|
||||||
// shift-alt-drag the original, we shouldn't duplicate the copy too:
|
// shift-alt-drag the original, we shouldn't duplicate the copy too:
|
||||||
|
@ -103,7 +103,7 @@ describe('TLSelectTool.Translating', () => {
|
||||||
expect(editor.selectedShapeIds).toStrictEqual([ids.box1])
|
expect(editor.selectedShapeIds).toStrictEqual([ids.box1])
|
||||||
editor.pointerMove(250, 150)
|
editor.pointerMove(250, 150)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.shapesOnCurrentPage.length).toStrictEqual(3)
|
expect(editor.currentPageShapes.length).toStrictEqual(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ describe('When double clicking a shape', () => {
|
||||||
.deleteShapes(editor.selectedShapeIds)
|
.deleteShapes(editor.selectedShapeIds)
|
||||||
.selectNone()
|
.selectNone()
|
||||||
.createShapes([{ id: createShapeId(), type: 'geo' }])
|
.createShapes([{ id: createShapeId(), type: 'geo' }])
|
||||||
.doubleClick(50, 50, { target: 'shape', shape: editor.shapesOnCurrentPage[0] })
|
.doubleClick(50, 50, { target: 'shape', shape: editor.currentPageShapes[0] })
|
||||||
.expectToBeIn('select.editing_shape')
|
.expectToBeIn('select.editing_shape')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -358,45 +358,45 @@ describe('When editing shapes', () => {
|
||||||
it('Double clicking the canvas creates a new text shape', () => {
|
it('Double clicking the canvas creates a new text shape', () => {
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
expect(editor.selectedShapeIds.length).toBe(0)
|
expect(editor.selectedShapeIds.length).toBe(0)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
expect(editor.currentPageShapes.length).toBe(5)
|
||||||
editor.doubleClick(750, 750)
|
editor.doubleClick(750, 750)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
expect(editor.currentPageShapes.length).toBe(6)
|
||||||
expect(editor.shapesOnCurrentPage[5].type).toBe('text')
|
expect(editor.currentPageShapes[5].type).toBe('text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('It deletes an empty text shape when your click away', () => {
|
it('It deletes an empty text shape when your click away', () => {
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
expect(editor.selectedShapeIds.length).toBe(0)
|
expect(editor.selectedShapeIds.length).toBe(0)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
expect(editor.currentPageShapes.length).toBe(5)
|
||||||
|
|
||||||
// Create a new shape by double clicking
|
// Create a new shape by double clicking
|
||||||
editor.doubleClick(750, 750)
|
editor.doubleClick(750, 750)
|
||||||
expect(editor.selectedShapeIds.length).toBe(1)
|
expect(editor.selectedShapeIds.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
expect(editor.currentPageShapes.length).toBe(6)
|
||||||
const shapeId = editor.selectedShapeIds[0]
|
const shapeId = editor.selectedShapeIds[0]
|
||||||
|
|
||||||
// Click away
|
// Click away
|
||||||
editor.click(1000, 1000)
|
editor.click(1000, 1000)
|
||||||
expect(editor.selectedShapeIds.length).toBe(0)
|
expect(editor.selectedShapeIds.length).toBe(0)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
expect(editor.currentPageShapes.length).toBe(5)
|
||||||
expect(editor.getShape(shapeId)).toBe(undefined)
|
expect(editor.getShape(shapeId)).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('It deletes an empty text shape when your click another text shape', () => {
|
it('It deletes an empty text shape when your click another text shape', () => {
|
||||||
expect(editor.editingShapeId).toBe(null)
|
expect(editor.editingShapeId).toBe(null)
|
||||||
expect(editor.selectedShapeIds.length).toBe(0)
|
expect(editor.selectedShapeIds.length).toBe(0)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
expect(editor.currentPageShapes.length).toBe(5)
|
||||||
|
|
||||||
// Create a new shape by double clicking
|
// Create a new shape by double clicking
|
||||||
editor.doubleClick(750, 750)
|
editor.doubleClick(750, 750)
|
||||||
expect(editor.selectedShapeIds.length).toBe(1)
|
expect(editor.selectedShapeIds.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
expect(editor.currentPageShapes.length).toBe(6)
|
||||||
const shapeId = editor.selectedShapeIds[0]
|
const shapeId = editor.selectedShapeIds[0]
|
||||||
|
|
||||||
// Click another text shape
|
// Click another text shape
|
||||||
editor.click(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) })
|
editor.click(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) })
|
||||||
expect(editor.selectedShapeIds.length).toBe(1)
|
expect(editor.selectedShapeIds.length).toBe(1)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
expect(editor.currentPageShapes.length).toBe(5)
|
||||||
expect(editor.getShape(shapeId)).toBe(undefined)
|
expect(editor.getShape(shapeId)).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -219,7 +219,10 @@ describe('<TldrawEditor />', () => {
|
||||||
|
|
||||||
expect(editor).toBeTruthy()
|
expect(editor).toBeTruthy()
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
|
editor.updateInstanceState(
|
||||||
|
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
|
@ -340,7 +343,10 @@ describe('Custom shapes', () => {
|
||||||
|
|
||||||
expect(editor).toBeTruthy()
|
expect(editor).toBeTruthy()
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
|
editor.updateInstanceState(
|
||||||
|
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||||
|
{ ephemeral: true, squashing: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(editor.shapeUtils.card).toBeTruthy()
|
expect(editor.shapeUtils.card).toBeTruthy()
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -36,7 +36,7 @@ describe('Making an arrow on the page', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerMove(0, 0)
|
editor.pointerMove(0, 0)
|
||||||
editor.pointerDown()
|
editor.pointerDown()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cleans up the arrow if the user did not start dragging', () => {
|
it('cleans up the arrow if the user did not start dragging', () => {
|
||||||
|
@ -44,24 +44,24 @@ describe('Making an arrow on the page', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerMove(0, 0)
|
editor.pointerMove(0, 0)
|
||||||
editor.click()
|
editor.click()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
// with double click
|
// with double click
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerMove(0, 0)
|
editor.pointerMove(0, 0)
|
||||||
editor.doubleClick()
|
editor.doubleClick()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
// with pointer up
|
// with pointer up
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown()
|
editor.pointerDown()
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
|
|
||||||
// did not add it to the history stack
|
// did not add it to the history stack
|
||||||
editor.undo()
|
editor.undo()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
editor.redo()
|
editor.redo()
|
||||||
editor.redo()
|
editor.redo()
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
expect(editor.currentPageShapes.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the arrow if the user dragged', () => {
|
it('keeps the arrow if the user dragged', () => {
|
||||||
|
@ -75,7 +75,7 @@ describe('Making an arrow on the page', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 0)
|
editor.pointerDown(0, 0)
|
||||||
editor.pointerMove(100, 0)
|
editor.pointerMove(100, 0)
|
||||||
const arrow1 = editor.shapesOnCurrentPage[0]
|
const arrow1 = editor.currentPageShapes[0]
|
||||||
|
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
|
@ -262,25 +262,25 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
it('does not create the arrow immediately', () => {
|
it('does not create the arrow immediately', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not create a shape if pointer up before drag', () => {
|
it('does not create a shape if pointer up before drag', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
editor.pointerUp(50, 50)
|
editor.pointerUp(50, 50)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates the arrow after a drag, bound to the shape', () => {
|
it('creates the arrow after a drag, bound to the shape', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(50, 50)
|
editor.pointerDown(50, 50)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(55, 50)
|
editor.pointerMove(55, 50)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
|
@ -308,10 +308,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
it('always creates the arrow with an imprecise start point', () => {
|
it('always creates the arrow with an imprecise start point', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(20, 20) // upper left
|
editor.pointerDown(20, 20) // upper left
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(25, 20)
|
editor.pointerMove(25, 20)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 20,
|
x: 20,
|
||||||
y: 20,
|
y: 20,
|
||||||
|
@ -340,11 +340,11 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
it('after a pause before drag, creates an arrow with a precise start point', () => {
|
it('after a pause before drag, creates an arrow with a precise start point', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(20, 20) // upper left
|
editor.pointerDown(20, 20) // upper left
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
expect(editor.currentPageShapes.length).toBe(1)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
editor.pointerMove(25, 20)
|
editor.pointerMove(25, 20)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 20,
|
x: 20,
|
||||||
y: 20,
|
y: 20,
|
||||||
|
@ -383,10 +383,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
|
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(25, 25)
|
editor.pointerDown(25, 25)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
expect(editor.currentPageShapes.length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 25,
|
x: 25,
|
||||||
y: 25,
|
y: 25,
|
||||||
|
@ -403,8 +403,8 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
type: 'binding',
|
type: 'binding',
|
||||||
boundShapeId: ids.box2,
|
boundShapeId: ids.box2,
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.6,
|
x: 0.55,
|
||||||
y: 0.6,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -417,10 +417,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
|
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(25, 25)
|
editor.pointerDown(25, 25)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
expect(editor.currentPageShapes.length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 25,
|
x: 25,
|
||||||
y: 25,
|
y: 25,
|
||||||
|
@ -437,8 +437,8 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
type: 'binding',
|
type: 'binding',
|
||||||
boundShapeId: ids.box2,
|
boundShapeId: ids.box2,
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.6,
|
x: 0.55,
|
||||||
y: 0.6,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -462,10 +462,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
|
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(25, 25)
|
editor.pointerDown(25, 25)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
expect(editor.currentPageShapes.length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 25,
|
x: 25,
|
||||||
y: 25,
|
y: 25,
|
||||||
|
@ -498,10 +498,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
|
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(25, 25)
|
editor.pointerDown(25, 25)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
expect(editor.currentPageShapes.length).toBe(2)
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
expect(editor.currentPageShapes.length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({
|
||||||
x: 25,
|
x: 25,
|
||||||
y: 25,
|
y: 25,
|
||||||
|
@ -518,11 +518,87 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
type: 'binding',
|
type: 'binding',
|
||||||
boundShapeId: ids.box2,
|
boundShapeId: ids.box2,
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.6,
|
// kicked over because it was too close to the center, and we can't have both bound there
|
||||||
y: 0.6,
|
x: 0.55,
|
||||||
|
y: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
editor.pointerMove(35, 35)
|
||||||
|
expect(editor.currentPageShapes.length).toBe(3)
|
||||||
|
expect(arrow()).toMatchObject({
|
||||||
|
x: 25,
|
||||||
|
y: 25,
|
||||||
|
props: {
|
||||||
|
start: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: ids.box2,
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: 0.5,
|
||||||
|
y: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: ids.box2,
|
||||||
|
normalizedAnchor: {
|
||||||
|
// kicked over because it was too close to the center, and we can't have both bound there
|
||||||
|
x: 0.7,
|
||||||
|
y: 0.7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When deleting shapes with bound arrows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.createShapes([
|
||||||
|
{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100, fill: 'solid' } },
|
||||||
|
])
|
||||||
|
editor.createShapes([
|
||||||
|
{ id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100, fill: 'solid' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('also removes the binding', () => {
|
||||||
|
function arrow() {
|
||||||
|
return editor.currentPageShapes.find((s) => s.type === 'arrow') as TLArrowShape
|
||||||
|
}
|
||||||
|
|
||||||
|
editor
|
||||||
|
// create arrow from box1 to box2
|
||||||
|
.setCurrentTool('arrow')
|
||||||
|
.pointerMove(50, 50)
|
||||||
|
|
||||||
|
editor.history.clear()
|
||||||
|
|
||||||
|
editor.pointerDown()
|
||||||
|
|
||||||
|
expect(editor.history._undos.value.length).toBe(0)
|
||||||
|
|
||||||
|
editor.pointerMove(250, 50)
|
||||||
|
|
||||||
|
editor.pointerUp()
|
||||||
|
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
|
||||||
|
// select box2
|
||||||
|
editor.click(275, 25)
|
||||||
|
|
||||||
|
editor.mark('deleting')
|
||||||
|
editor.deleteShapes([ids.box2])
|
||||||
|
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
|
||||||
|
editor.undo()
|
||||||
|
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
|
||||||
|
editor.redo()
|
||||||
|
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
129
packages/tldraw/src/test/cleanup.test.ts
Normal file
129
packages/tldraw/src/test/cleanup.test.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { TLArrowShape, createShapeId } from '@tldraw/editor'
|
||||||
|
import { TestEditor } from './TestEditor'
|
||||||
|
|
||||||
|
let editor: TestEditor
|
||||||
|
|
||||||
|
const ids = {
|
||||||
|
box1: createShapeId('box1'),
|
||||||
|
box2: createShapeId('box2'),
|
||||||
|
box3: createShapeId('box3'),
|
||||||
|
box4: createShapeId('box4'),
|
||||||
|
box5: createShapeId('box5'),
|
||||||
|
frame1: createShapeId('frame1'),
|
||||||
|
group1: createShapeId('group1'),
|
||||||
|
group2: createShapeId('group2'),
|
||||||
|
group3: createShapeId('group3'),
|
||||||
|
arrow1: createShapeId('arrow1'),
|
||||||
|
arrow2: createShapeId('arrow2'),
|
||||||
|
arrow3: createShapeId('arrow3'),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
function arrow() {
|
||||||
|
return editor.currentPageShapes.find((s) => s.type === 'arrow') as TLArrowShape
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('restoring bound arrows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.createShapes([
|
||||||
|
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
|
||||||
|
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
|
||||||
|
])
|
||||||
|
// create arrow from box1 to box2
|
||||||
|
editor
|
||||||
|
.setCurrentTool('arrow')
|
||||||
|
.pointerMove(50, 50)
|
||||||
|
.pointerDown()
|
||||||
|
.pointerMove(250, 50)
|
||||||
|
.pointerUp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes bound arrows on delete, restores them on undo but only when change was done by user', () => {
|
||||||
|
editor.mark('deleting')
|
||||||
|
editor.deleteShapes([ids.box2])
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
editor.undo()
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
editor.redo()
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes / restores multiple bindings', () => {
|
||||||
|
editor.mark('deleting')
|
||||||
|
expect(arrow().props.start.type).toBe('binding')
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
|
||||||
|
editor.deleteShapes([ids.box1, ids.box2])
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
|
||||||
|
editor.undo()
|
||||||
|
expect(arrow().props.start.type).toBe('binding')
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
|
||||||
|
editor.redo()
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('restoring bound arrows multiplayer', () => {
|
||||||
|
it('restores bound arrows after the shape was deleted by a different client', () => {
|
||||||
|
editor.mark()
|
||||||
|
editor.createShapes([{ id: ids.box2, type: 'geo', x: 100, y: 0 }])
|
||||||
|
|
||||||
|
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
|
||||||
|
|
||||||
|
// console.log(JSON.stringify(editor.history._undos.value.toArray(), null, 2))
|
||||||
|
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
|
||||||
|
// Merge a change from a remote source that deletes box 2
|
||||||
|
editor.store.mergeRemoteChanges(() => {
|
||||||
|
editor.store.remove([ids.box2])
|
||||||
|
})
|
||||||
|
|
||||||
|
// box is gone
|
||||||
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
|
// arrow is still there, but without its binding
|
||||||
|
expect(arrow()).not.toBeUndefined()
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
|
||||||
|
editor.undo() // undo creating the arrow
|
||||||
|
|
||||||
|
// arrow is gone too now
|
||||||
|
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||||
|
|
||||||
|
editor.redo() // redo creating the arrow
|
||||||
|
|
||||||
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
|
expect(arrow()).not.toBeUndefined()
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('point')
|
||||||
|
|
||||||
|
editor.undo() // undo creating arrow
|
||||||
|
|
||||||
|
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||||
|
|
||||||
|
editor.undo() // undo creating box
|
||||||
|
|
||||||
|
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||||
|
|
||||||
|
editor.redo() // redo creating box
|
||||||
|
|
||||||
|
// box is back! arrow is gone
|
||||||
|
expect(editor.getShape(ids.box2)).not.toBeUndefined()
|
||||||
|
expect(arrow()).toBeUndefined()
|
||||||
|
|
||||||
|
editor.redo() // redo creating arrow
|
||||||
|
|
||||||
|
// box is back! arrow should be bound
|
||||||
|
expect(arrow().props.start.type).toBe('point')
|
||||||
|
expect(arrow().props.end.type).toBe('binding')
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue