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) {
|
||||
// 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) {
|
||||
|
|
|
@ -23,7 +23,7 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
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)
|
||||
|
||||
await page.keyboard.down('Control')
|
||||
|
@ -32,7 +32,7 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.keyboard.press('KeyV')
|
||||
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)
|
||||
})
|
||||
|
||||
|
@ -42,7 +42,7 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
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)
|
||||
|
||||
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.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)
|
||||
})
|
||||
|
||||
|
@ -63,7 +63,7 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
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)
|
||||
|
||||
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.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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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.
|
||||
|
||||
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).
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import { TLAssetPartial } from '@tldraw/tlschema';
|
|||
import { TLBaseShape } from '@tldraw/tlschema';
|
||||
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
||||
import { TLCamera } from '@tldraw/tlschema';
|
||||
import { TLCameraId } from '@tldraw/tlschema';
|
||||
import { TLCursorType } from '@tldraw/tlschema';
|
||||
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
|
||||
import { TLDocument } from '@tldraw/tlschema';
|
||||
|
@ -48,10 +49,13 @@ import { TLHandle } from '@tldraw/tlschema';
|
|||
import { TLImageAsset } from '@tldraw/tlschema';
|
||||
import { TLInstance } from '@tldraw/tlschema';
|
||||
import { TLInstancePageState } from '@tldraw/tlschema';
|
||||
import { TLInstancePageStateId } from '@tldraw/tlschema';
|
||||
import { TLInstancePresence } from '@tldraw/tlschema';
|
||||
import { TLPage } from '@tldraw/tlschema';
|
||||
import { TLPageId } from '@tldraw/tlschema';
|
||||
import { TLParentId } from '@tldraw/tlschema';
|
||||
import { TLPointer } from '@tldraw/tlschema';
|
||||
import { TLPointerId } from '@tldraw/tlschema';
|
||||
import { TLRecord } from '@tldraw/tlschema';
|
||||
import { TLScribble } 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;
|
||||
// (undocumented)
|
||||
alignShapes(ids: TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
animateCamera(x: number, y: number, z?: number, opts?: TLAnimationOptions): this;
|
||||
animateShape(partial: null | TLShapePartial | undefined, options?: Partial<{
|
||||
duration: number;
|
||||
ease: (t: number) => number;
|
||||
|
@ -561,6 +564,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// (undocumented)
|
||||
bringToFront(ids: TLShapeId[]): this;
|
||||
get camera(): TLCamera;
|
||||
get cameraId(): TLCameraId;
|
||||
get cameraState(): "idle" | "moving";
|
||||
cancel(): this;
|
||||
cancelDoubleClick(): void;
|
||||
|
@ -568,7 +572,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
get canUndo(): boolean;
|
||||
// @internal (undocumented)
|
||||
capturedPointerId: null | number;
|
||||
centerOnPoint(x: number, y: number, opts?: TLAnimationOptions): this;
|
||||
centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this;
|
||||
// (undocumented)
|
||||
readonly cleanup: CleanupManager;
|
||||
// @internal
|
||||
protected _clickManager: ClickManager;
|
||||
get commonBoundsOfAllShapesOnCurrentPage(): Box2d | undefined;
|
||||
|
@ -577,7 +583,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
crash(error: unknown): void;
|
||||
// @internal
|
||||
get crashingError(): unknown;
|
||||
createAssets(assets: TLAsset[]): this;
|
||||
createAsset(asset: TLAsset): this;
|
||||
createAssets(assets: TLAsset[], opts?: {
|
||||
ephemeral?: boolean;
|
||||
squashing?: boolean;
|
||||
}): this;
|
||||
// @internal (undocumented)
|
||||
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
||||
tags: {
|
||||
|
@ -592,21 +602,40 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
};
|
||||
};
|
||||
createPage(title: string, id?: TLPageId, belowPageIndex?: string): this;
|
||||
createShape<T extends TLUnknownShape>(partial: TLShapePartial<T>, select?: boolean): this;
|
||||
createShapes<T extends TLUnknownShape>(partials: TLShapePartial<T>[], select?: boolean): this;
|
||||
// (undocumented)
|
||||
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 currentPage(): TLPage;
|
||||
get currentPageId(): TLPageId;
|
||||
get currentPageShapeIds(): Set<TLShapeId>;
|
||||
get currentPageShapes(): TLShape[];
|
||||
get currentPageShapesSorted(): TLShape[];
|
||||
get currentPageState(): TLInstancePageState;
|
||||
get currentPageStateId(): TLInstancePageStateId;
|
||||
get currentTool(): StateNode | undefined;
|
||||
get currentToolId(): string;
|
||||
deleteAsset(assets: TLAsset): this;
|
||||
// (undocumented)
|
||||
deleteAsset(ids: TLAssetId): this;
|
||||
deleteAssets(assets: TLAsset[]): this;
|
||||
// (undocumented)
|
||||
deleteAssets(ids: TLAssetId[]): this;
|
||||
deleteOpenMenu(id: string): this;
|
||||
deletePage(page: TLPage): this;
|
||||
// (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;
|
||||
// (undocumented)
|
||||
deleteShape(shape: TLShape): this;
|
||||
|
@ -629,9 +658,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
duplicateShapes(shapes: TLShape[], offset?: VecLike): this;
|
||||
// (undocumented)
|
||||
duplicateShapes(ids: TLShapeId[], offset?: VecLike): this;
|
||||
// (undocumented)
|
||||
get editingShape(): TLUnknownShape | undefined;
|
||||
get editingShapeId(): null | TLShapeId;
|
||||
readonly environment: EnvironmentManager;
|
||||
get erasingShapeIds(): TLShapeId[];
|
||||
get erasingShapeIdsSet(): Set<TLShapeId>;
|
||||
// (undocumented)
|
||||
get erasingShapes(): NonNullable<TLShape | undefined>[];
|
||||
// @internal (undocumented)
|
||||
externalAssetContentHandlers: {
|
||||
[K in TLExternalAssetContent_2['type']]: {
|
||||
|
@ -712,6 +745,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getPageMask(id: TLShapeId): undefined | VecLike[];
|
||||
// (undocumented)
|
||||
getPageMask(shape: TLShape): undefined | VecLike[];
|
||||
getPageShapeIds(page: TLPage): Set<TLShapeId>;
|
||||
// (undocumented)
|
||||
getPageShapeIds(pageId: TLPageId): Set<TLShapeId>;
|
||||
getPageState(pageId: TLPageId): TLInstancePageState;
|
||||
getPageTransform(id: TLShapeId): Matrix2d;
|
||||
// (undocumented)
|
||||
getPageTransform(shape: TLShape): Matrix2d;
|
||||
|
@ -739,9 +776,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
hitFrameInside?: boolean | undefined;
|
||||
filter?: ((shape: TLShape) => boolean) | undefined;
|
||||
}): TLShape | undefined;
|
||||
getShapeIdsInPage(page: TLPage): Set<TLShapeId>;
|
||||
// (undocumented)
|
||||
getShapeIdsInPage(pageId: TLPageId): Set<TLShapeId>;
|
||||
getShapesAtPoint(point: VecLike, opts?: {
|
||||
margin?: number | undefined;
|
||||
hitInside?: boolean | undefined;
|
||||
|
@ -776,6 +810,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// (undocumented)
|
||||
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
||||
get hintingShapeIds(): TLShapeId[];
|
||||
// (undocumented)
|
||||
get hintingShapes(): NonNullable<TLShape | undefined>[];
|
||||
readonly history: HistoryManager<this>;
|
||||
// (undocumented)
|
||||
get hoveredShape(): TLUnknownShape | undefined;
|
||||
|
@ -805,12 +841,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
isAncestorSelected(id: TLShapeId): boolean;
|
||||
// (undocumented)
|
||||
isAncestorSelected(shape: TLShape): boolean;
|
||||
readonly isAndroid: boolean;
|
||||
readonly isChromeForIos: boolean;
|
||||
readonly isFirefox: boolean;
|
||||
isIn(path: string): boolean;
|
||||
isInAny(...paths: string[]): boolean;
|
||||
readonly isIos: boolean;
|
||||
get isMenuOpen(): boolean;
|
||||
isPointInShape(shape: TLShape, point: VecLike, opts?: {
|
||||
margin?: number;
|
||||
|
@ -821,7 +853,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
margin?: number;
|
||||
hitInside?: boolean;
|
||||
}): boolean;
|
||||
readonly isSafari: boolean;
|
||||
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
|
||||
// (undocumented)
|
||||
isShapeInPage(shapeId: TLShapeId, pageId?: TLPageId): boolean;
|
||||
|
@ -831,7 +862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
isShapeOrAncestorLocked(shape?: TLShape): boolean;
|
||||
// (undocumented)
|
||||
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;
|
||||
// (undocumented)
|
||||
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
|
||||
|
@ -845,13 +876,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
packShapes(ids: TLShapeId[], gap: number): this;
|
||||
get pages(): TLPage[];
|
||||
get pageStates(): TLInstancePageState[];
|
||||
pageToScreen(x: number, y: number, z?: number, camera?: VecLike): {
|
||||
pageToScreen(point: VecLike): {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
pan(dx: number, dy: number, opts?: TLAnimationOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], opts?: TLAnimationOptions): this;
|
||||
pan(offset: VecLike, animation?: TLAnimationOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this;
|
||||
popFocusLayer(): this;
|
||||
putContent(content: TLContent, options?: {
|
||||
point?: VecLike;
|
||||
|
@ -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 & {
|
||||
type: T;
|
||||
} : 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 renderingBoundsExpanded(): Box2d;
|
||||
// (undocumented)
|
||||
renderingBoundsMargin: number;
|
||||
get renderingShapes(): {
|
||||
id: TLShapeId;
|
||||
shape: TLShape;
|
||||
|
@ -883,10 +913,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
isInViewport: boolean;
|
||||
maskedPageBounds: Box2d | undefined;
|
||||
}[];
|
||||
reparentShapes(shapes: TLShape[], parentId: TLParentId, insertIndex?: string): this;
|
||||
reparentShapes(shapes: TLShape[], parentId: TLParentId, opts?: {
|
||||
insertIndex?: string;
|
||||
} & CommandHistoryOptions): this;
|
||||
// (undocumented)
|
||||
reparentShapes(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
||||
resetZoom(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
reparentShapes(ids: TLShapeId[], parentId: TLParentId, opts?: {
|
||||
insertIndex?: string;
|
||||
} & CommandHistoryOptions): this;
|
||||
resetZoom(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
resizeShape(id: TLShapeId, scale: VecLike, options?: {
|
||||
initialBounds?: Box2d;
|
||||
scaleOrigin?: VecLike;
|
||||
|
@ -900,7 +934,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
rotateShapesBy(shapes: TLShape[], delta: number): this;
|
||||
// (undocumented)
|
||||
rotateShapesBy(ids: TLShapeId[], delta: number): this;
|
||||
screenToPage(x: number, y: number, z?: number, camera?: VecLike): {
|
||||
screenToPage(point: VecLike): {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
|
@ -921,32 +955,29 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
sendToBack(shapes: TLShape[]): this;
|
||||
// (undocumented)
|
||||
sendToBack(ids: TLShapeId[]): this;
|
||||
setCamera(x: number, y: number, z?: number, { stopFollowing }?: TLViewportOptions): this;
|
||||
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
|
||||
// (undocumented)
|
||||
setCroppingId(id: null | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage, opts?: TLViewportOptions): this;
|
||||
setCroppingShapeId(id: null | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage): this;
|
||||
// (undocumented)
|
||||
setCurrentPage(pageId: TLPageId, opts?: TLViewportOptions): this;
|
||||
setCurrentPage(pageId: TLPageId): this;
|
||||
setCurrentTool(id: string, info?: {}): this;
|
||||
// (undocumented)
|
||||
setEditingId(id: null | TLShapeId): this;
|
||||
setEditingShapeId(id: null | TLShapeId): this;
|
||||
// (undocumented)
|
||||
setErasingIds(ids: TLShapeId[]): this;
|
||||
setErasingShapeIds(ids: TLShapeId[]): this;
|
||||
// (undocumented)
|
||||
setFocusedGroupId(next: TLPageId | TLShapeId): this;
|
||||
setFocusedGroupId(id: TLPageId | TLShapeId): this;
|
||||
// (undocumented)
|
||||
setHintingIds(ids: TLShapeId[]): this;
|
||||
setHintingShapeIds(ids: TLShapeId[]): this;
|
||||
// (undocumented)
|
||||
setHoveredId(id: null | TLShapeId): this;
|
||||
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
||||
setHoveredShapeId(id: null | TLShapeId): this;
|
||||
setOpacity(opacity: number, opts?: CommandHistoryOptions): this;
|
||||
setSelectedShapeIds(ids: TLShapeId[], squashing?: boolean): this;
|
||||
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
|
||||
get shapeIdsOnCurrentPage(): Set<TLShapeId>;
|
||||
get shapesOnCurrentPage(): TLShape[];
|
||||
setStyle<T>(style: StyleProp<T>, value: T, opts?: CommandHistoryOptions): this;
|
||||
shapeUtils: {
|
||||
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
||||
};
|
||||
get sharedOpacity(): SharedStyle<number>;
|
||||
get sharedStyles(): ReadonlySharedStyleMap;
|
||||
slideCamera(opts?: {
|
||||
speed: number;
|
||||
|
@ -955,7 +986,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
speedThreshold?: number | undefined;
|
||||
}): this | undefined;
|
||||
readonly snaps: SnapManager;
|
||||
get sortedShapesOnCurrentPage(): TLShape[];
|
||||
stackShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical', gap: number): this;
|
||||
// (undocumented)
|
||||
stackShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
|
||||
|
@ -974,19 +1004,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
toggleLock(shapes: TLShape[]): this;
|
||||
// (undocumented)
|
||||
toggleLock(ids: TLShapeId[]): this;
|
||||
undo(): HistoryManager<this>;
|
||||
undo(): this;
|
||||
ungroupShapes(ids: TLShapeId[]): this;
|
||||
// (undocumented)
|
||||
ungroupShapes(ids: TLShape[]): this;
|
||||
updateAssets(assets: TLAssetPartial[]): this;
|
||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, ephemeral?: boolean): this;
|
||||
updateAsset(partial: TLAssetPartial, opts?: {
|
||||
ephemeral?: boolean;
|
||||
squashing?: boolean;
|
||||
}): this;
|
||||
updateAssets(partials: TLAssetPartial[], opts?: {
|
||||
ephemeral?: boolean;
|
||||
squashing?: boolean;
|
||||
}): 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;
|
||||
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
|
||||
updateRenderingBounds(): this;
|
||||
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, squashing?: boolean): this;
|
||||
updateShapes<T extends TLUnknownShape>(partials: (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)[], opts?: CommandHistoryOptions): this;
|
||||
updateViewportScreenBounds(center?: boolean): this;
|
||||
readonly user: UserPreferencesManager;
|
||||
get viewportPageBounds(): Box2d;
|
||||
|
@ -996,13 +1039,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
visitDescendants(parent: TLPage | TLShape, visitor: (id: TLShapeId) => false | void): this;
|
||||
// (undocumented)
|
||||
visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => false | void): this;
|
||||
zoomIn(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
zoomIn(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
get zoomLevel(): number;
|
||||
zoomOut(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||
zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: TLAnimationOptions): this;
|
||||
zoomOut(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
zoomToBounds(bounds: Box2d, targetZoom?: number, animation?: TLAnimationOptions): this;
|
||||
zoomToContent(): this;
|
||||
zoomToFit(opts?: TLAnimationOptions): this;
|
||||
zoomToSelection(opts?: TLAnimationOptions): this;
|
||||
zoomToFit(animation?: TLAnimationOptions): this;
|
||||
zoomToSelection(animation?: TLAnimationOptions): this;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1628,7 +1671,7 @@ export function refreshPage(): void;
|
|||
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export type RequiredKeys<T, K extends keyof T> = Pick<T, K> & Partial<T>;
|
||||
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function resizeBox(shape: TLBaseBoxShape, info: {
|
||||
|
|
|
@ -33,7 +33,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
|||
'schema' in rest
|
||||
? rest.schema
|
||||
: createTLSchema({
|
||||
shapes: shapesOnCurrentPageToShapeMap(checkShapesAndAddCore(rest.shapeUtils)),
|
||||
shapes: currentPageShapesToShapeMap(checkShapesAndAddCore(rest.shapeUtils)),
|
||||
})
|
||||
return new Store({
|
||||
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(
|
||||
shapeUtils.map((s): [string, SchemaShapeInfo] => [
|
||||
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'
|
||||
|
||||
function createCounterHistoryManager() {
|
||||
const manager = new HistoryManager(
|
||||
{ emit: () => void null },
|
||||
() => null,
|
||||
() => {
|
||||
return
|
||||
}
|
||||
)
|
||||
const manager = new HistoryManager({ emit: () => void null }, () => {
|
||||
return
|
||||
})
|
||||
const state = {
|
||||
count: 0,
|
||||
name: 'David',
|
||||
|
@ -251,7 +247,10 @@ describe(HistoryManager, () => {
|
|||
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()
|
||||
expect(editor.history.numUndos).toBe(1)
|
||||
expect(editor.getCount()).toBe(2)
|
||||
|
@ -260,7 +259,7 @@ describe(HistoryManager, () => {
|
|||
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.incrementTwice()
|
||||
editor.history.mark('2')
|
||||
|
|
|
@ -4,13 +4,17 @@ import { uniqueId } from '../../utils/uniqueId'
|
|||
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
||||
import { Stack, stack } from './Stack'
|
||||
|
||||
/** @public */
|
||||
export type CommandHistoryOptions = Partial<{
|
||||
squashing: boolean
|
||||
ephemeral: boolean
|
||||
preservesRedoStack: boolean
|
||||
}>
|
||||
|
||||
type CommandFn<Data> = (...args: any[]) =>
|
||||
| {
|
||||
| ({
|
||||
data: Data
|
||||
squashing?: boolean
|
||||
ephemeral?: boolean
|
||||
preservesRedoStack?: boolean
|
||||
}
|
||||
} & CommandHistoryOptions)
|
||||
| null
|
||||
| undefined
|
||||
| void
|
||||
|
@ -29,7 +33,6 @@ export class HistoryManager<
|
|||
|
||||
constructor(
|
||||
private readonly ctx: CTX,
|
||||
private readonly onBatchComplete: () => void,
|
||||
private readonly annotateError: (error: unknown) => void
|
||||
) {}
|
||||
|
||||
|
@ -43,6 +46,8 @@ export class HistoryManager<
|
|||
return this._redos.value.length
|
||||
}
|
||||
|
||||
skipHistory = false
|
||||
|
||||
createCommand = <Name extends string, Constructor extends CommandFn<any>>(
|
||||
name: Name,
|
||||
constructor: Constructor,
|
||||
|
@ -68,13 +73,14 @@ export class HistoryManager<
|
|||
|
||||
const { data, ephemeral, squashing, preservesRedoStack } = result
|
||||
|
||||
this.ignoringUpdates((undos, redos) => {
|
||||
handle.do(data)
|
||||
return { undos, redos }
|
||||
})
|
||||
// this.ignoringUpdates((undos, redos) => {
|
||||
handle.do(data)
|
||||
// return { undos, redos }
|
||||
// })
|
||||
|
||||
if (!ephemeral) {
|
||||
const prev = this._undos.value.head
|
||||
|
||||
if (
|
||||
squashing &&
|
||||
prev &&
|
||||
|
@ -103,6 +109,7 @@ export class HistoryManager<
|
|||
)
|
||||
}
|
||||
|
||||
// clear the redo stack unless the command explicitly says not to
|
||||
if (!result.preservesRedoStack) {
|
||||
this._redos.set(stack())
|
||||
}
|
||||
|
@ -116,7 +123,19 @@ export class HistoryManager<
|
|||
return exec
|
||||
}
|
||||
|
||||
onBatchStart = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
onBatchComplete = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
batch = (fn: () => void) => {
|
||||
if (this._batchDepth === 0) {
|
||||
this.onBatchStart()
|
||||
}
|
||||
|
||||
try {
|
||||
this._batchDepth++
|
||||
if (this._batchDepth === 1) {
|
||||
|
|
|
@ -313,15 +313,15 @@ export class SnapManager {
|
|||
|
||||
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
|
||||
})
|
||||
|
||||
// Collect horizontal gaps
|
||||
for (let i = 0; i < sortedShapesOnCurrentPageHorizontal.length; i++) {
|
||||
startNode = sortedShapesOnCurrentPageHorizontal[i]
|
||||
for (let j = i + 1; j < sortedShapesOnCurrentPageHorizontal.length; j++) {
|
||||
endNode = sortedShapesOnCurrentPageHorizontal[j]
|
||||
for (let i = 0; i < currentPageShapesSortedHorizontal.length; i++) {
|
||||
startNode = currentPageShapesSortedHorizontal[i]
|
||||
for (let j = i + 1; j < currentPageShapesSortedHorizontal.length; j++) {
|
||||
endNode = currentPageShapesSortedHorizontal[j]
|
||||
|
||||
if (
|
||||
// is there space between the boxes
|
||||
|
@ -358,14 +358,14 @@ export class SnapManager {
|
|||
}
|
||||
|
||||
// Collect vertical gaps
|
||||
const sortedShapesOnCurrentPageVertical = sortedShapesOnCurrentPageHorizontal.sort((a, b) => {
|
||||
const currentPageShapesSortedVertical = currentPageShapesSortedHorizontal.sort((a, b) => {
|
||||
return a.pageBounds.minY - b.pageBounds.minY
|
||||
})
|
||||
|
||||
for (let i = 0; i < sortedShapesOnCurrentPageVertical.length; i++) {
|
||||
startNode = sortedShapesOnCurrentPageVertical[i]
|
||||
for (let j = i + 1; j < sortedShapesOnCurrentPageVertical.length; j++) {
|
||||
endNode = sortedShapesOnCurrentPageVertical[j]
|
||||
for (let i = 0; i < currentPageShapesSortedVertical.length; i++) {
|
||||
startNode = currentPageShapesSortedVertical[i]
|
||||
for (let j = i + 1; j < currentPageShapesSortedVertical.length; j++) {
|
||||
endNode = currentPageShapesSortedVertical[j]
|
||||
|
||||
if (
|
||||
// is there space between the boxes
|
||||
|
|
|
@ -52,12 +52,12 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
component(shape: TLGroupShape) {
|
||||
// Not a class component, but eslint can't tell that :(
|
||||
const {
|
||||
erasingShapeIdsSet,
|
||||
erasingShapeIds,
|
||||
currentPageState: { hintingShapeIds, focusedGroupId },
|
||||
zoomLevel,
|
||||
} = this.editor
|
||||
|
||||
const isErasing = erasingShapeIdsSet.has(shape.id)
|
||||
const isErasing = erasingShapeIds.includes(shape.id)
|
||||
|
||||
const isHintingOtherGroup =
|
||||
hintingShapeIds.length > 0 &&
|
||||
|
|
|
@ -9,7 +9,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
|
|
|
@ -29,21 +29,19 @@ export class Pointing extends StateNode {
|
|||
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.createShapes<TLBaseBoxShape>(
|
||||
[
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
props: {
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
this.editor.createShapes<TLBaseBoxShape>([
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
props: {
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
},
|
||||
])
|
||||
this.editor.select(id)
|
||||
this.editor.setCurrentTool('select.resizing', {
|
||||
...info,
|
||||
target: 'selection',
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
/** @public */
|
||||
export type RequiredKeys<T, K extends keyof T> = Pick<T, K> & Partial<T>
|
||||
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
|
||||
/** @public */
|
||||
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
|
|
@ -84,12 +84,14 @@ export function useCanvasEvents() {
|
|||
|
||||
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 point = editor.screenToPage({ x: e.clientX - rect.x, y: e.clientY - rect.y })
|
||||
|
||||
await editor.putExternalContent({
|
||||
type: 'files',
|
||||
files,
|
||||
point: editor.screenToPage(e.clientX - rect.x, e.clientY - rect.y),
|
||||
point,
|
||||
ignoreParent: false,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ export function useCoarsePointer() {
|
|||
// 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
|
||||
// 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 })
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export function useZoomCss() {
|
|||
const setScaleDebounced = debounce(setScale, 100)
|
||||
|
||||
const scheduler = new EffectScheduler('useZoomCss', () => {
|
||||
const numShapes = editor.shapeIdsOnCurrentPage.size
|
||||
const numShapes = editor.currentPageShapeIds.size
|
||||
if (numShapes < 300) {
|
||||
setScale(editor.zoomLevel)
|
||||
} else {
|
||||
|
|
|
@ -44,7 +44,7 @@ export interface AtomOptions<Value, Diff> {
|
|||
* ```ts
|
||||
* const name = atom('name', 'John')
|
||||
*
|
||||
* console.log(name.value) // 'John'
|
||||
* print(name.value) // 'John'
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
|
|
|
@ -26,7 +26,7 @@ type UNINITIALIZED = typeof UNINITIALIZED
|
|||
* const count = atom('count', 0)
|
||||
* const double = computed('double', (prevValue) => {
|
||||
* if (isUninitialized(prevValue)) {
|
||||
* console.log('First time!')
|
||||
* print('First time!')
|
||||
* }
|
||||
* return count.value * 2
|
||||
* })
|
||||
|
@ -296,7 +296,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
|
|||
* ```ts
|
||||
* const name = atom('name', 'John')
|
||||
* 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.
|
||||
|
|
|
@ -39,7 +39,7 @@ let stack: CaptureStackFrame | null = null
|
|||
* })
|
||||
*
|
||||
* 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')
|
||||
* react('greeting', () => {
|
||||
* whyAmIRunning()
|
||||
* console.log('Hello', name.value)
|
||||
* print('Hello', name.value)
|
||||
* })
|
||||
*
|
||||
* name.set('Alice')
|
||||
|
|
|
@ -143,7 +143,7 @@ export let currentTransaction = null as Transaction | null
|
|||
* const lastName = atom('Doe')
|
||||
*
|
||||
* react('greet', () => {
|
||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* })
|
||||
*
|
||||
* // Logs "Hello, John Doe!"
|
||||
|
@ -164,7 +164,7 @@ export let currentTransaction = null as Transaction | null
|
|||
* const lastName = atom('Doe')
|
||||
*
|
||||
* react('greet', () => {
|
||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* })
|
||||
*
|
||||
* // Logs "Hello, John Doe!"
|
||||
|
@ -187,7 +187,7 @@ export let currentTransaction = null as Transaction | null
|
|||
* const lastName = atom('Doe')
|
||||
*
|
||||
* react('greet', () => {
|
||||
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* print(`Hello, ${firstName.value} ${lastName.value}!`)
|
||||
* })
|
||||
*
|
||||
* // Logs "Hello, John Doe!"
|
||||
|
|
|
@ -244,6 +244,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
// (undocumented)
|
||||
_flushHistory(): void;
|
||||
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
|
||||
// (undocumented)
|
||||
getRecordType: <T extends R>(record: R) => T;
|
||||
getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
|
||||
has: <K extends IdOf<R>>(id: K) => boolean;
|
||||
readonly history: Atom<number, RecordsDiff<R>>;
|
||||
|
@ -255,10 +257,12 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
// @internal (undocumented)
|
||||
markAsPossiblyCorrupted(): void;
|
||||
mergeRemoteChanges: (fn: () => void) => void;
|
||||
onAfterChange?: (prev: R, next: R) => void;
|
||||
onAfterCreate?: (record: R) => void;
|
||||
onAfterDelete?: (prev: R) => void;
|
||||
onBeforeDelete?: (prev: R) => void;
|
||||
onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void;
|
||||
onAfterCreate?: (record: R, source: 'remote' | 'user') => void;
|
||||
onAfterDelete?: (prev: R, source: 'remote' | 'user') => 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)
|
||||
readonly props: Props;
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* records in the store.
|
||||
*
|
||||
* @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.
|
||||
|
@ -311,21 +327,21 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
* @param prev - The previous value, if any.
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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
|
||||
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).
|
||||
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++) {
|
||||
record = records[i]
|
||||
|
||||
const recordAtom = (map ?? currentMap)[record.id as IdOf<R>]
|
||||
|
||||
if (recordAtom) {
|
||||
if (beforeUpdate) record = beforeUpdate(recordAtom.value, record, source)
|
||||
|
||||
// If we already have an atom for this record, update its value.
|
||||
|
||||
const initialValue = recordAtom.__unsafe__getWithoutCapture()
|
||||
|
@ -382,6 +404,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
updates[record.id] = [initialValue, finalValue]
|
||||
}
|
||||
} else {
|
||||
if (beforeCreate) record = beforeCreate(record, source)
|
||||
|
||||
didChange = true
|
||||
|
||||
// If we don't have an atom, create one.
|
||||
|
@ -418,20 +442,22 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
removed: {} as Record<IdOf<R>, R>,
|
||||
})
|
||||
|
||||
const { onAfterCreate, onAfterChange } = this
|
||||
if (this._runCallbacks) {
|
||||
const { onAfterCreate, onAfterChange } = this
|
||||
|
||||
if (onAfterCreate && this._runCallbacks) {
|
||||
// Run the onAfterChange callback for addition.
|
||||
Object.values(additions).forEach((record) => {
|
||||
onAfterCreate(record)
|
||||
})
|
||||
}
|
||||
if (onAfterCreate) {
|
||||
// Run the onAfterChange callback for addition.
|
||||
Object.values(additions).forEach((record) => {
|
||||
onAfterCreate(record, source)
|
||||
})
|
||||
}
|
||||
|
||||
if (onAfterChange && this._runCallbacks) {
|
||||
// Run the onAfterChange callback for update.
|
||||
Object.values(updates).forEach(([from, to]) => {
|
||||
onAfterChange(from, to)
|
||||
})
|
||||
if (onAfterChange) {
|
||||
// Run the onAfterChange callback for update.
|
||||
Object.values(updates).forEach(([from, to]) => {
|
||||
onAfterChange(from, to, source)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -444,12 +470,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
*/
|
||||
remove = (ids: IdOf<R>[]): void => {
|
||||
transact(() => {
|
||||
const cancelled = [] as IdOf<R>[]
|
||||
const source = this.isMergingRemoteChanges ? 'remote' : 'user'
|
||||
|
||||
if (this.onBeforeDelete && this._runCallbacks) {
|
||||
for (const id of ids) {
|
||||
const atom = this.atoms.__unsafe__getWithoutCapture()[id]
|
||||
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
|
||||
|
||||
for (const id of ids) {
|
||||
if (cancelled.includes(id)) continue
|
||||
if (!(id in atoms)) continue
|
||||
if (!result) result = { ...atoms }
|
||||
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 (this.onAfterDelete && this._runCallbacks) {
|
||||
let record: R
|
||||
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`)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -39,9 +39,9 @@ it('enters the arrow state', () => {
|
|||
|
||||
describe('When in the idle state', () => {
|
||||
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)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
editor.expectPathToBe('root.arrow.pointing')
|
||||
})
|
||||
|
@ -55,18 +55,18 @@ describe('When in the idle state', () => {
|
|||
|
||||
describe('When in the pointing state', () => {
|
||||
it('cancels on pointer up', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerUp(0, 0)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.arrow.idle')
|
||||
})
|
||||
|
||||
it('bails on cancel', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor.setCurrentTool('arrow').pointerDown(0, 0).cancel()
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.arrow.idle')
|
||||
|
@ -82,7 +82,7 @@ describe('When in the pointing state', () => {
|
|||
describe('When dragging the arrow', () => {
|
||||
it('updates the arrow on pointer move', () => {
|
||||
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, {
|
||||
id: arrow.id,
|
||||
type: 'arrow',
|
||||
|
@ -97,9 +97,9 @@ describe('When dragging the arrow', () => {
|
|||
})
|
||||
|
||||
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)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
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', () => {
|
||||
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)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.arrow.idle')
|
||||
})
|
||||
|
||||
it('bails on cancel', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(10, 10).cancel()
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore)
|
||||
editor.expectPathToBe('root.arrow.idle')
|
||||
})
|
||||
|
@ -139,7 +139,7 @@ describe('When pointing a start shape', () => {
|
|||
// Clear hinting ids when moving away
|
||||
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, {
|
||||
id: arrow.id,
|
||||
type: 'arrow',
|
||||
|
@ -179,7 +179,7 @@ describe('When pointing an end shape', () => {
|
|||
// Set hinting id when pointing the shape
|
||||
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, {
|
||||
id: arrow.id,
|
||||
type: 'arrow',
|
||||
|
@ -208,7 +208,7 @@ describe('When pointing an end shape', () => {
|
|||
|
||||
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)
|
||||
|
||||
|
@ -230,7 +230,7 @@ describe('When pointing an end shape', () => {
|
|||
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
||||
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||
|
||||
editor.expectShapeToMatch(arrow, {
|
||||
id: arrow.id,
|
||||
|
@ -250,7 +250,7 @@ describe('When pointing an end shape', () => {
|
|||
|
||||
editor.pointerMove(375, 0)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
||||
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||
|
||||
editor.expectShapeToMatch(arrow, {
|
||||
id: arrow.id,
|
||||
|
@ -268,7 +268,7 @@ describe('When pointing an end shape', () => {
|
|||
editor.pointerMove(325, 325)
|
||||
expect(editor.hintingShapeIds.length).toBe(1)
|
||||
|
||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
||||
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||
|
||||
editor.expectShapeToMatch(arrow, {
|
||||
id: arrow.id,
|
||||
|
@ -289,7 +289,7 @@ describe('When pointing an end shape', () => {
|
|||
// Give time for the velocity to die down
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
||||
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||
|
||||
editor.expectShapeToMatch(arrow, {
|
||||
id: arrow.id,
|
||||
|
@ -316,7 +316,7 @@ describe('When pointing an end shape', () => {
|
|||
editor.inputs.pointerVelocity = new Vec2d(1, 1)
|
||||
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)
|
||||
|
||||
|
@ -340,7 +340,7 @@ describe('When pointing an end shape', () => {
|
|||
it('begins precise when moving slowly', () => {
|
||||
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, {
|
||||
id: arrow.id,
|
||||
|
@ -358,7 +358,7 @@ describe('When pointing an end shape', () => {
|
|||
editor.inputs.pointerVelocity = new Vec2d(0.001, 0.001)
|
||||
editor.pointerMove(375, 375)
|
||||
|
||||
arrow = editor.shapesOnCurrentPage[editor.shapesOnCurrentPage.length - 1]
|
||||
arrow = editor.currentPageShapes[editor.currentPageShapes.length - 1]
|
||||
|
||||
expect(editor.hintingShapeIds.length).toBe(1)
|
||||
|
||||
|
@ -390,7 +390,7 @@ describe('reparenting issue', () => {
|
|||
editor.pointerMove(100, 100)
|
||||
editor.pointerUp()
|
||||
|
||||
const arrowId = editor.sortedShapesOnCurrentPage[0].id
|
||||
const arrowId = editor.currentPageShapesSorted[0].id
|
||||
|
||||
// Now create three shapes
|
||||
editor.createShapes([
|
||||
|
|
|
@ -302,7 +302,7 @@ describe('Other cases when arrow are moved', () => {
|
|||
.groupShapes(editor.selectedShapeIds)
|
||||
|
||||
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(arrow.props.end.type === 'binding')
|
||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
||||
|
@ -322,7 +322,7 @@ describe('When a shape it rotated', () => {
|
|||
it('binds correctly', () => {
|
||||
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({
|
||||
props: {
|
||||
|
@ -371,8 +371,8 @@ describe('resizing', () => {
|
|||
.pointerUp()
|
||||
.setCurrentTool('select')
|
||||
|
||||
const arrow1 = editor.shapesOnCurrentPage.at(-2)!
|
||||
const arrow2 = editor.shapesOnCurrentPage.at(-1)!
|
||||
const arrow1 = editor.currentPageShapes.at(-2)!
|
||||
const arrow2 = editor.currentPageShapes.at(-1)!
|
||||
|
||||
editor
|
||||
.select(arrow1.id, arrow2.id)
|
||||
|
@ -426,8 +426,8 @@ describe('resizing', () => {
|
|||
.pointerUp()
|
||||
.setCurrentTool('select')
|
||||
|
||||
const arrow1 = editor.shapesOnCurrentPage.at(-2)!
|
||||
const arrow2 = editor.shapesOnCurrentPage.at(-1)!
|
||||
const arrow1 = editor.currentPageShapes.at(-2)!
|
||||
const arrow2 = editor.currentPageShapes.at(-1)!
|
||||
|
||||
editor.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }])
|
||||
|
||||
|
@ -551,6 +551,7 @@ describe("an arrow's parents", () => {
|
|||
})
|
||||
// move b outside of frame
|
||||
editor.select(boxBid).translateSelection(200, 0)
|
||||
jest.advanceTimersByTime(500)
|
||||
expect(editor.getShape(arrowId)).toMatchObject({
|
||||
parentId: editor.currentPageId,
|
||||
props: {
|
||||
|
@ -565,6 +566,10 @@ describe("an arrow's parents", () => {
|
|||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
||||
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({
|
||||
parentId: editor.currentPageId,
|
||||
props: {
|
||||
|
@ -576,6 +581,12 @@ describe("an arrow's parents", () => {
|
|||
// move c inside of frame
|
||||
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({
|
||||
parentId: frameId,
|
||||
props: {
|
||||
|
|
|
@ -495,7 +495,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
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
|
||||
}, [shape])
|
||||
|
||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
|
|
|
@ -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 {
|
||||
static override id = 'pointing'
|
||||
|
@ -7,6 +7,8 @@ export class Pointing extends StateNode {
|
|||
|
||||
markId = ''
|
||||
|
||||
initialEndHandle = {} as TLHandle
|
||||
|
||||
override onEnter = () => {
|
||||
this.didTimeout = false
|
||||
|
||||
|
@ -19,7 +21,7 @@ export class Pointing extends StateNode {
|
|||
if (!target) {
|
||||
this.createArrowShape()
|
||||
} else {
|
||||
this.editor.setHintingIds([target.id])
|
||||
this.editor.setHintingShapeIds([target.id])
|
||||
}
|
||||
|
||||
this.startPreciseTimeout()
|
||||
|
@ -27,7 +29,7 @@ export class Pointing extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
this.shape = undefined
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
this.clearPreciseTimeout()
|
||||
}
|
||||
|
||||
|
@ -43,7 +45,7 @@ export class Pointing extends StateNode {
|
|||
|
||||
this.editor.setCurrentTool('select.dragging_handle', {
|
||||
shape: this.shape,
|
||||
handle: this.editor.getHandles(this.shape)!.find((h) => h.id === 'end')!,
|
||||
handle: this.initialEndHandle,
|
||||
isCreating: true,
|
||||
onInteractionEnd: 'arrow',
|
||||
})
|
||||
|
@ -71,7 +73,7 @@ export class Pointing extends StateNode {
|
|||
// the arrow might not have been created yet!
|
||||
this.editor.bailToMark(this.markId)
|
||||
}
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
||||
|
@ -80,9 +82,9 @@ export class Pointing extends StateNode {
|
|||
|
||||
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,
|
||||
type: 'arrow',
|
||||
|
@ -107,28 +109,27 @@ export class Pointing extends StateNode {
|
|||
if (change) {
|
||||
const startTerminal = change.props?.start
|
||||
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
|
||||
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() {
|
||||
const shape = this.shape
|
||||
if (!shape) throw Error(`expected shape`)
|
||||
|
||||
const handles = this.editor.getHandles(shape)
|
||||
if (!handles) throw Error(`expected handles for arrow`)
|
||||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||
|
||||
// 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 endHandle = handles.find((h) => h.id === 'end')!
|
||||
const endHandle = this.editor.getHandles(shape)!.find((h) => h.id === 'end')!
|
||||
const change = util.onHandleChange?.(shape, {
|
||||
handle: { ...endHandle, x: point.x, y: point.y },
|
||||
isPrecise: false, // sure about that?
|
||||
|
@ -137,28 +138,28 @@ export class Pointing extends StateNode {
|
|||
if (change) {
|
||||
const endTerminal = change.props?.end
|
||||
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
|
||||
{
|
||||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||
const startHandle = handles.find((h) => h.id === 'start')!
|
||||
const shape = this.editor.getShape(this.shape!.id)! as TLArrowShape
|
||||
const startHandle = this.editor.getHandles(shape)!.find((h) => h.id === 'start')!
|
||||
const change = util.onHandleChange?.(shape, {
|
||||
handle: { ...startHandle, x: 0, y: 0 },
|
||||
isPrecise: this.didTimeout, // sure about that?
|
||||
})
|
||||
|
||||
if (change) {
|
||||
this.editor.updateShapes([change], true)
|
||||
this.editor.updateShape(change, { squashing: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -424,7 +424,7 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial], true)
|
||||
this.editor.updateShape(shapePartial, { squashing: true })
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -566,7 +566,7 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial], true)
|
||||
this.editor.updateShape(shapePartial, { squashing: true })
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
@ -8,7 +8,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
|
|
|
@ -12,44 +12,44 @@ afterEach(() => {
|
|||
|
||||
describe(FrameShapeTool, () => {
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerMove(100, 100)
|
||||
editor.pointerUp(100, 100)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('frame')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('frame')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
editor.redo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
})
|
||||
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('frame')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('frame')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
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', () => {
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('frame')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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', () => {
|
||||
editor.updateInstanceState({ isToolLocked: true })
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('frame')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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
|
||||
e.stopPropagation()
|
||||
e.currentTarget.blur()
|
||||
editor.setEditingId(null)
|
||||
editor.setEditingShapeId(null)
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
|
@ -30,16 +30,7 @@ export const FrameLabelInput = forwardRef<
|
|||
const value = e.currentTarget.value.trim()
|
||||
if (name === value) return
|
||||
|
||||
editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
type: 'frame',
|
||||
props: { name: value },
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
editor.updateShape({ id, type: 'frame', props: { name: value } }, { squashing: true })
|
||||
},
|
||||
[id, editor]
|
||||
)
|
||||
|
@ -53,16 +44,7 @@ export const FrameLabelInput = forwardRef<
|
|||
const value = e.currentTarget.value
|
||||
if (name === value) return
|
||||
|
||||
editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
type: 'frame',
|
||||
props: { name: value },
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
editor.updateShape({ id, type: 'frame', props: { name: value } }, { squashing: true })
|
||||
},
|
||||
[id, editor]
|
||||
)
|
||||
|
|
|
@ -12,44 +12,44 @@ afterEach(() => {
|
|||
|
||||
describe(GeoShapeTool, () => {
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerMove(100, 100)
|
||||
editor.pointerUp()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('geo')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('geo')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
editor.redo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
})
|
||||
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('geo')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('geo')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
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.pointerUp(200, 200)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
|
||||
editor.selectAll()
|
||||
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', () => {
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('geo')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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', () => {
|
||||
editor.updateInstanceState({ isToolLocked: true })
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('geo')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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 = () => {
|
||||
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) => {
|
||||
|
@ -17,7 +20,7 @@ export class Idle extends StateNode {
|
|||
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
|
||||
this.editor.mark('editing shape')
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setEditingShapeId(shape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
|
|
|
@ -15,9 +15,9 @@ it('enters the line state', () => {
|
|||
|
||||
describe('When in the idle state', () => {
|
||||
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' })
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
editor.expectPathToBe('root.line.pointing')
|
||||
})
|
||||
|
@ -31,18 +31,18 @@ describe('When in the idle state', () => {
|
|||
|
||||
describe('When in the pointing state', () => {
|
||||
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)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.line.idle')
|
||||
})
|
||||
|
||||
it('bails on cancel', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor.setCurrentTool('line').pointerDown(0, 0, { target: 'canvas' }).cancel()
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.line.idle')
|
||||
|
@ -58,7 +58,7 @@ describe('When in the pointing state', () => {
|
|||
describe('When dragging the line', () => {
|
||||
it('updates the line on pointer move', () => {
|
||||
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, {
|
||||
id: line.id,
|
||||
type: 'line',
|
||||
|
@ -75,13 +75,13 @@ describe('When dragging the line', () => {
|
|||
})
|
||||
|
||||
it('returns to select.idle, keeping shape, on pointer up', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor
|
||||
.setCurrentTool('line')
|
||||
.pointerDown(0, 0, { target: 'canvas' })
|
||||
.pointerMove(10, 10)
|
||||
.pointerUp(10, 10)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
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', () => {
|
||||
editor.updateInstanceState({ isToolLocked: true })
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor
|
||||
.setCurrentTool('line')
|
||||
.pointerDown(0, 0, { target: 'canvas' })
|
||||
.pointerMove(10, 10)
|
||||
.pointerUp(10, 10)
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore + 1)
|
||||
expect(editor.hintingShapeIds.length).toBe(0)
|
||||
editor.expectPathToBe('root.line.idle')
|
||||
})
|
||||
|
||||
it('bails on cancel', () => {
|
||||
const shapesBefore = editor.shapesOnCurrentPage.length
|
||||
const shapesBefore = editor.currentPageShapes.length
|
||||
editor
|
||||
.setCurrentTool('line')
|
||||
.pointerDown(0, 0, { target: 'canvas' })
|
||||
.pointerMove(10, 10)
|
||||
.cancel()
|
||||
const shapesAfter = editor.shapesOnCurrentPage.length
|
||||
const shapesAfter = editor.currentPageShapes.length
|
||||
expect(shapesAfter).toBe(shapesBefore)
|
||||
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' })
|
||||
.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'))
|
||||
const handles = Object.values(line.props.handles)
|
||||
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)
|
||||
.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'))
|
||||
const handles = Object.values(line.props.handles)
|
||||
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)
|
||||
.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'))
|
||||
const handles = Object.values(line.props.handles)
|
||||
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)
|
||||
.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'))
|
||||
const handles = Object.values(line.props.handles)
|
||||
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)
|
||||
.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'))
|
||||
const handles = Object.values(line.props.handles)
|
||||
expect(handles.length).toBe(3)
|
||||
|
|
|
@ -62,7 +62,7 @@ describe('Translating', () => {
|
|||
editor.select(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.pointerMove(300, 400) // Move shape by 50, 150
|
||||
|
@ -195,7 +195,7 @@ describe('Misc', () => {
|
|||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||
editor.pointerUp().keyUp('Alt')
|
||||
|
||||
expect(Array.from(editor.shapeIdsOnCurrentPage.values()).length).toEqual(2)
|
||||
expect(Array.from(editor.currentPageShapeIds.values()).length).toEqual(2)
|
||||
})
|
||||
|
||||
it('deletes', () => {
|
||||
|
@ -207,7 +207,7 @@ describe('Misc', () => {
|
|||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||
editor.pointerUp().keyUp('Alt')
|
||||
|
||||
let ids = Array.from(editor.shapeIdsOnCurrentPage.values())
|
||||
let ids = Array.from(editor.currentPageShapeIds.values())
|
||||
expect(ids.length).toEqual(2)
|
||||
|
||||
const duplicate = ids.filter((i) => i !== id)[0]
|
||||
|
@ -215,7 +215,7 @@ describe('Misc', () => {
|
|||
|
||||
editor.deleteShapes(editor.selectedShapeIds)
|
||||
|
||||
ids = Array.from(editor.shapeIdsOnCurrentPage.values())
|
||||
ids = Array.from(editor.currentPageShapeIds.values())
|
||||
expect(ids.length).toEqual(1)
|
||||
expect(ids[0]).toEqual(id)
|
||||
})
|
||||
|
|
|
@ -7,7 +7,7 @@ Object {
|
|||
"isLocked": false,
|
||||
"meta": Object {},
|
||||
"opacity": 1,
|
||||
"parentId": "page:id51",
|
||||
"parentId": "page:id60",
|
||||
"props": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
|
|
|
@ -7,7 +7,10 @@ export class Idle extends StateNode {
|
|||
|
||||
override onEnter = (info: { shapeId: TLShapeId }) => {
|
||||
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'] = () => {
|
||||
|
|
|
@ -30,7 +30,8 @@ export class Pointing extends StateNode {
|
|||
const shape = info.shapeId && this.editor.getShape<TLLineShape>(info.shapeId)
|
||||
|
||||
if (shape) {
|
||||
this.markId = this.editor.mark(`creating:${shape.id}`)
|
||||
this.markId = `creating:${shape.id}`
|
||||
this.editor.mark(this.markId)
|
||||
this.shape = shape
|
||||
|
||||
if (inputs.shiftKey) {
|
||||
|
@ -85,18 +86,20 @@ export class Pointing extends StateNode {
|
|||
} else {
|
||||
const id = createShapeId()
|
||||
|
||||
this.markId = this.editor.mark(`creating:${id}`)
|
||||
this.markId = `creating:${id}`
|
||||
|
||||
this.editor.createShapes<TLLineShape>([
|
||||
{
|
||||
id,
|
||||
type: 'line',
|
||||
x: currentPagePoint.x,
|
||||
y: currentPagePoint.y,
|
||||
},
|
||||
])
|
||||
this.editor
|
||||
.mark(this.markId)
|
||||
.createShapes<TLLineShape>([
|
||||
{
|
||||
id,
|
||||
type: 'line',
|
||||
x: currentPagePoint.x,
|
||||
y: currentPagePoint.y,
|
||||
},
|
||||
])
|
||||
.select(id)
|
||||
|
||||
this.editor.select(id)
|
||||
this.shape = this.editor.getShape(id)!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,47 +12,47 @@ afterEach(() => {
|
|||
|
||||
describe(NoteShapeTool, () => {
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerMove(100, 100)
|
||||
editor.pointerUp(100, 100)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('note')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('note')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.cancel() // leave edit mode
|
||||
|
||||
editor.undo() // undoes the selection change
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
editor.redo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
})
|
||||
|
||||
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.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage[0]?.type).toBe('note')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.shapesOnCurrentPage[0]?.id)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(editor.currentPageShapes[0]?.type).toBe('note')
|
||||
expect(editor.selectedShapeIds[0]).toBe(editor.currentPageShapes[0]?.id)
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
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', () => {
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('note')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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', () => {
|
||||
editor.updateInstanceState({ isToolLocked: true })
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('note')
|
||||
editor.pointerDown(50, 50)
|
||||
editor.pointerUp(50, 50)
|
||||
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 = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'cross', rotation: 0 } },
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
|
|
|
@ -65,7 +65,7 @@ export class Pointing extends StateNode {
|
|||
if (this.editor.instanceState.isToolLocked) {
|
||||
this.parent.transition('idle', {})
|
||||
} else {
|
||||
this.editor.setEditingId(this.shape.id)
|
||||
this.editor.setEditingShapeId(this.shape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape', {
|
||||
...this.info,
|
||||
target: 'shape',
|
||||
|
@ -87,19 +87,17 @@ export class Pointing extends StateNode {
|
|||
|
||||
const id = createShapeId()
|
||||
this.markId = `creating:${id}`
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.createShapes(
|
||||
[
|
||||
this.editor
|
||||
.mark(this.markId)
|
||||
.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'note',
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
])
|
||||
.select(id)
|
||||
|
||||
const shape = this.editor.getShape<TLNoteShape>(id)!
|
||||
const bounds = this.editor.getGeometry(shape).bounds
|
||||
|
|
|
@ -250,7 +250,7 @@ function PatternFillDefForCanvas() {
|
|||
const { defs, isReady } = usePattern()
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && editor.isSafari) {
|
||||
if (isReady && editor.environment.isSafari) {
|
||||
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
||||
if (htmlLayer) {
|
||||
// Wait for `patternContext` to be picked up
|
||||
|
|
|
@ -211,8 +211,8 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
(e: React.PointerEvent) => {
|
||||
if (isEditableFromHover) {
|
||||
transact(() => {
|
||||
editor.setEditingId(id)
|
||||
editor.setHoveredId(id)
|
||||
editor.setEditingShapeId(id)
|
||||
editor.setHoveredShapeId(id)
|
||||
editor.setSelectedShapeIds([id])
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ afterEach(() => {
|
|||
|
||||
describe(TextShapeTool, () => {
|
||||
it('Creates text, edits it, undoes and redoes', () => {
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.setCurrentTool('text')
|
||||
editor.expectToBeIn('text.idle')
|
||||
editor.pointerDown(0, 0)
|
||||
|
@ -22,28 +22,28 @@ describe(TextShapeTool, () => {
|
|||
editor.expectToBeIn('select.editing_shape')
|
||||
// This comes from the component, not the state chart
|
||||
editor.updateShapes([
|
||||
{ ...editor.shapesOnCurrentPage[0]!, type: 'text', props: { text: 'Hello' } },
|
||||
{ ...editor.currentPageShapes[0]!, type: 'text', props: { text: 'Hello' } },
|
||||
])
|
||||
// Deselect the editing shape
|
||||
editor.cancel()
|
||||
editor.expectToBeIn('select.idle')
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
editor.expectShapeToMatch({
|
||||
id: editor.shapesOnCurrentPage[0].id,
|
||||
id: editor.currentPageShapes[0].id,
|
||||
type: 'text',
|
||||
props: { text: 'Hello' },
|
||||
})
|
||||
|
||||
editor.undo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
editor.redo()
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
|
||||
editor.expectShapeToMatch({
|
||||
id: editor.shapesOnCurrentPage[0].id,
|
||||
id: editor.currentPageShapes[0].id,
|
||||
type: 'text',
|
||||
props: { text: 'Hello' },
|
||||
})
|
||||
|
@ -71,7 +71,7 @@ describe('When in idle state', () => {
|
|||
editor.pointerDown(0, 0)
|
||||
editor.pointerUp()
|
||||
editor.expectToBeIn('select.editing_shape')
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
})
|
||||
|
||||
it('returns to select on cancel', () => {
|
||||
|
@ -87,7 +87,7 @@ describe('When in the pointing state', () => {
|
|||
editor.pointerDown(0, 0)
|
||||
editor.cancel()
|
||||
editor.expectToBeIn('text.idle')
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('returns to idle on interrupt', () => {
|
||||
|
@ -96,7 +96,7 @@ describe('When in the pointing state', () => {
|
|||
editor.expectToBeIn('text.pointing')
|
||||
editor.interrupt()
|
||||
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', () => {
|
||||
|
@ -105,7 +105,7 @@ describe('When in the pointing state', () => {
|
|||
editor.pointerMove(10, 10)
|
||||
editor.expectToBeIn('select.resizing')
|
||||
editor.pointerUp()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
editor.expectToBeIn('select.editing_shape')
|
||||
})
|
||||
|
||||
|
@ -115,8 +115,8 @@ describe('When in the pointing state', () => {
|
|||
const y = 0
|
||||
editor.pointerDown(x, y)
|
||||
editor.pointerUp()
|
||||
const bounds = editor.getPageBounds(editor.shapesOnCurrentPage[0])!
|
||||
expect(editor.shapesOnCurrentPage[0]).toMatchObject({
|
||||
const bounds = editor.getPageBounds(editor.currentPageShapes[0])!
|
||||
expect(editor.currentPageShapes[0]).toMatchObject({
|
||||
x: x - bounds.width / 2,
|
||||
y: y - bounds.height / 2,
|
||||
})
|
||||
|
@ -131,7 +131,7 @@ describe('When resizing', () => {
|
|||
editor.expectToBeIn('select.resizing')
|
||||
editor.cancel()
|
||||
editor.expectToBeIn('text.idle')
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('does not bails on interrupt while resizing', () => {
|
||||
|
@ -140,7 +140,7 @@ describe('When resizing', () => {
|
|||
editor.pointerMove(100, 100)
|
||||
editor.expectToBeIn('select.resizing')
|
||||
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', () => {
|
||||
|
@ -149,7 +149,7 @@ describe('When resizing', () => {
|
|||
const y = 0
|
||||
editor.pointerDown(x, y)
|
||||
editor.pointerMove(x + 100, y + 100)
|
||||
expect(editor.shapesOnCurrentPage[0]).toMatchObject({
|
||||
expect(editor.currentPageShapes[0]).toMatchObject({
|
||||
x,
|
||||
y,
|
||||
})
|
||||
|
|
|
@ -23,7 +23,7 @@ export class Idle extends StateNode {
|
|||
if (this.editor.isShapeOfType<TLTextShape>(hitShape, 'text')) {
|
||||
requestAnimationFrame(() => {
|
||||
this.editor.setSelectedShapeIds([hitShape.id])
|
||||
this.editor.setEditingId(hitShape.id)
|
||||
this.editor.setEditingShapeId(hitShape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
|
@ -38,7 +38,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
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) => {
|
||||
|
@ -46,7 +49,7 @@ export class Idle extends StateNode {
|
|||
const shape = this.editor.selectedShapes[0]
|
||||
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setEditingShapeId(shape.id)
|
||||
this.editor.root.current.value!.transition('editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
|
|
|
@ -8,7 +8,7 @@ export class Pointing extends StateNode {
|
|||
markId = ''
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
|
@ -19,23 +19,23 @@ export class Pointing extends StateNode {
|
|||
|
||||
const id = createShapeId()
|
||||
|
||||
this.markId = this.editor.mark(`creating:${id}`)
|
||||
|
||||
this.editor.createShapes<TLTextShape>([
|
||||
{
|
||||
id,
|
||||
type: 'text',
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
props: {
|
||||
text: '',
|
||||
autoSize: false,
|
||||
w: 20,
|
||||
this.markId = `creating:${id}`
|
||||
this.editor
|
||||
.mark(this.markId)
|
||||
.createShapes<TLTextShape>([
|
||||
{
|
||||
id,
|
||||
type: 'text',
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
props: {
|
||||
text: '',
|
||||
autoSize: false,
|
||||
w: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
this.editor.select(id)
|
||||
])
|
||||
.select(id)
|
||||
|
||||
this.shape = this.editor.getShape(id)
|
||||
if (!this.shape) return
|
||||
|
@ -72,8 +72,8 @@ export class Pointing extends StateNode {
|
|||
this.editor.mark('creating text shape')
|
||||
const id = createShapeId()
|
||||
const { x, y } = this.editor.inputs.currentPagePoint
|
||||
this.editor.createShapes(
|
||||
[
|
||||
this.editor
|
||||
.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'text',
|
||||
|
@ -84,11 +84,10 @@ export class Pointing extends StateNode {
|
|||
autoSize: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
])
|
||||
.select(id)
|
||||
|
||||
this.editor.setEditingId(id)
|
||||
this.editor.setEditingShapeId(id)
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.root.current.value?.transition('editing_shape', {})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ export class EraserTool extends StateNode {
|
|||
static override children = () => [Idle, Pointing, Erasing]
|
||||
|
||||
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>()
|
||||
|
||||
override onEnter = (info: TLPointerEventInfo) => {
|
||||
this.markId = this.editor.mark('erase scribble begin')
|
||||
this.info = info
|
||||
|
||||
this.markId = 'erase scribble begin'
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
this.excludedShapeIds = new Set(
|
||||
this.editor.shapesOnCurrentPage
|
||||
this.editor.currentPageShapes
|
||||
.filter(
|
||||
(shape) =>
|
||||
this.editor.isShapeOrAncestorLocked(shape) ||
|
||||
|
@ -95,8 +97,8 @@ export class Erasing extends StateNode {
|
|||
update() {
|
||||
const {
|
||||
zoomLevel,
|
||||
shapesOnCurrentPage,
|
||||
erasingShapeIdsSet,
|
||||
currentPageShapes,
|
||||
erasingShapeIds,
|
||||
inputs: { currentPagePoint, previousPagePoint },
|
||||
} = this.editor
|
||||
|
||||
|
@ -104,9 +106,9 @@ export class Erasing extends StateNode {
|
|||
|
||||
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
|
||||
|
||||
// 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
|
||||
// (these excluded shapes will be any frames or groups the pointer was inside of
|
||||
// when the user started erasing)
|
||||
this.editor.setErasingIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
||||
this.editor.setErasingShapeIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.editor.deleteShapes(this.editor.currentPageState.erasingShapeIds)
|
||||
this.editor.setErasingIds([])
|
||||
this.editor.setErasingShapeIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.editor.setErasingIds([])
|
||||
this.editor.setErasingShapeIds([])
|
||||
this.editor.bailToMark(this.markId)
|
||||
this.parent.transition('idle', this.info)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class Pointing extends StateNode {
|
|||
override onEnter = () => {
|
||||
const {
|
||||
inputs: { currentPagePoint },
|
||||
sortedShapesOnCurrentPage,
|
||||
currentPageShapesSorted,
|
||||
zoomLevel,
|
||||
} = this.editor
|
||||
|
||||
|
@ -21,8 +21,8 @@ export class Pointing extends StateNode {
|
|||
|
||||
const initialSize = erasing.size
|
||||
|
||||
for (let n = sortedShapesOnCurrentPage.length, i = n - 1; i >= 0; i--) {
|
||||
const shape = sortedShapesOnCurrentPage[i]
|
||||
for (let n = currentPageShapesSorted.length, i = n - 1; i >= 0; i--) {
|
||||
const shape = currentPageShapesSorted[i]
|
||||
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
||||
continue
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.editor.setErasingIds([...erasing])
|
||||
this.editor.setErasingShapeIds([...erasing])
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
|
@ -79,12 +79,12 @@ export class Pointing extends StateNode {
|
|||
this.editor.deleteShapes(erasingShapeIds)
|
||||
}
|
||||
|
||||
this.editor.setErasingIds([])
|
||||
this.editor.setErasingShapeIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.editor.setErasingIds([])
|
||||
this.editor.setErasingShapeIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class Dragging extends StateNode {
|
|||
const delta = Vec2d.Sub(currentScreenPoint, previousScreenPoint)
|
||||
|
||||
if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) {
|
||||
this.editor.pan(delta.x, delta.y)
|
||||
this.editor.pan(delta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@ export class Idle extends StateNode {
|
|||
static override id = 'idle'
|
||||
|
||||
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) => {
|
||||
|
|
|
@ -5,7 +5,10 @@ export class Pointing extends StateNode {
|
|||
|
||||
override onEnter = () => {
|
||||
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) => {
|
||||
|
|
|
@ -9,6 +9,9 @@ export class LaserTool extends StateNode {
|
|||
static override children = () => [Idle, Lasering]
|
||||
|
||||
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)
|
||||
|
||||
if (res && res.shouldHint) {
|
||||
this.editor.setHintingIds([nextDroppingShape.id])
|
||||
this.editor.setHintingShapeIds([nextDroppingShape.id])
|
||||
}
|
||||
} else {
|
||||
// If we're dropping onto the page, then clear hinting ids
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
}
|
||||
|
||||
cb?.()
|
||||
|
@ -103,7 +103,7 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
this.droppingNodeTimer = null
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
}
|
||||
|
||||
dispose = () => {
|
||||
|
|
|
@ -43,7 +43,7 @@ export class SelectTool extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
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.editor.shapesOnCurrentPage
|
||||
this.editor.currentPageShapes
|
||||
.filter(
|
||||
(shape) =>
|
||||
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||
|
@ -96,7 +96,7 @@ export class Brushing extends StateNode {
|
|||
const {
|
||||
zoomLevel,
|
||||
currentPageId,
|
||||
shapesOnCurrentPage,
|
||||
currentPageShapes,
|
||||
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
||||
} = this.editor
|
||||
|
||||
|
@ -118,8 +118,8 @@ export class Brushing extends StateNode {
|
|||
|
||||
const { excludedShapeIds } = this
|
||||
|
||||
testAllShapes: for (let i = 0, n = shapesOnCurrentPage.length; i < n; i++) {
|
||||
shape = shapesOnCurrentPage[i]
|
||||
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
||||
shape = currentPageShapes[i]
|
||||
if (excludedShapeIds.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'
|
||||
|
||||
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
|
||||
|
||||
|
@ -14,21 +17,22 @@ export class Idle extends StateNode {
|
|||
// (which clears the cropping id) but still remain in this state.
|
||||
this.editor.on('change-history', this.cleanupCroppingState)
|
||||
|
||||
this.editor.mark('crop')
|
||||
|
||||
if (onlySelectedShape) {
|
||||
this.editor.setCroppingId(onlySelectedShape.id)
|
||||
this.editor.setCroppingShapeId(onlySelectedShape.id)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.editor.setCurrentTool('select.idle', {})
|
||||
}
|
||||
|
||||
|
@ -36,7 +40,7 @@ export class Idle extends StateNode {
|
|||
if (this.editor.isMenuOpen) return
|
||||
|
||||
if (info.ctrlKey) {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.editor.setCurrentTool('select.brushing', info)
|
||||
return
|
||||
}
|
||||
|
@ -66,7 +70,7 @@ export class Idle extends StateNode {
|
|||
return
|
||||
} else {
|
||||
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.setCurrentTool('select.crop.pointing_crop', info)
|
||||
} else {
|
||||
|
@ -145,7 +149,7 @@ export class Idle extends StateNode {
|
|||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
switch (info.code) {
|
||||
case 'Enter': {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.editor.setCurrentTool('select.idle', {})
|
||||
break
|
||||
}
|
||||
|
@ -153,7 +157,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
private cancel() {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.editor.setCurrentTool('select.idle', {})
|
||||
}
|
||||
|
||||
|
|
|
@ -25,14 +25,20 @@ export class TranslatingCrop extends StateNode {
|
|||
) => {
|
||||
this.info = info
|
||||
this.snapshot = this.createSnapshot()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
|
@ -99,7 +105,7 @@ export class TranslatingCrop extends StateNode {
|
|||
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
||||
|
||||
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.markId = this.editor.mark('cropping')
|
||||
this.markId = 'cropping'
|
||||
this.editor.mark(this.markId)
|
||||
this.snapshot = this.createSnapshot()
|
||||
this.updateShapes()
|
||||
}
|
||||
|
@ -199,7 +200,7 @@ export class Cropping extends StateNode {
|
|||
},
|
||||
}
|
||||
|
||||
this.editor.updateShapes([partial], true)
|
||||
this.editor.updateShapes([partial], { squashing: true })
|
||||
this.updateCursor()
|
||||
}
|
||||
|
||||
|
@ -207,7 +208,7 @@ export class Cropping extends StateNode {
|
|||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +218,7 @@ export class Cropping extends StateNode {
|
|||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,8 @@ export class DraggingHandle extends StateNode {
|
|||
this.info = info
|
||||
this.parent.currentToolIdMask = info.onInteractionEnd
|
||||
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.initialPageTransform = this.editor.getPageTransform(shape)!
|
||||
this.initialPageRotation = this.initialPageTransform.rotation()
|
||||
|
@ -60,7 +61,7 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
|
||||
true
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
|
||||
// <!-- Only relevant to arrows
|
||||
|
@ -95,7 +96,7 @@ export class DraggingHandle extends StateNode {
|
|||
this.isPrecise = false
|
||||
|
||||
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 })
|
||||
if (this.isPrecise) {
|
||||
|
@ -104,7 +105,7 @@ export class DraggingHandle extends StateNode {
|
|||
this.resetExactTimeout()
|
||||
}
|
||||
} else {
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
}
|
||||
// -->
|
||||
|
||||
|
@ -166,9 +167,12 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
this.parent.currentToolIdMask = undefined
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setHintingShapeIds([])
|
||||
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() {
|
||||
|
@ -280,7 +284,7 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
if (bindingAfter?.type === 'binding') {
|
||||
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
|
||||
editor.setHintingIds([bindingAfter.boundShapeId])
|
||||
editor.setHintingShapeIds([bindingAfter.boundShapeId])
|
||||
this.pointingId = bindingAfter.boundShapeId
|
||||
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
|
||||
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
|
||||
|
@ -288,7 +292,7 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
} else {
|
||||
if (hintingShapeIds.length > 0) {
|
||||
editor.setHintingIds([])
|
||||
editor.setHintingShapeIds([])
|
||||
this.pointingId = null
|
||||
this.isPrecise = false
|
||||
this.isPreciseId = null
|
||||
|
@ -298,7 +302,7 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
// Clear the editing shape
|
||||
this.editor.setEditingId(null)
|
||||
this.editor.setEditingShapeId(null)
|
||||
|
||||
const shape = this.editor.getShape(editingShapeId)!
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
|
|
@ -23,7 +23,10 @@ export class Idle extends StateNode {
|
|||
|
||||
override onEnter = () => {
|
||||
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'] = () => {
|
||||
|
@ -438,7 +441,7 @@ export class Idle extends StateNode {
|
|||
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
||||
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||
this.editor.mark('editing shape')
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setEditingShapeId(shape.id)
|
||||
this.parent.transition('editing_shape', info)
|
||||
}
|
||||
|
||||
|
@ -467,7 +470,7 @@ export class Idle extends StateNode {
|
|||
const shape = this.editor.getShape(id)
|
||||
if (!shape) return
|
||||
|
||||
this.editor.setEditingId(id)
|
||||
this.editor.setEditingShapeId(id)
|
||||
this.editor.select(id)
|
||||
this.parent.transition('editing_shape', info)
|
||||
}
|
||||
|
|
|
@ -29,11 +29,14 @@ export class PointingCropHandle extends StateNode {
|
|||
if (!selectedShape) return
|
||||
|
||||
this.updateCursor(selectedShape)
|
||||
this.editor.setCroppingId(selectedShape.id)
|
||||
this.editor.setCroppingShapeId(selectedShape.id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -52,7 +55,7 @@ export class PointingCropHandle extends StateNode {
|
|||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +76,7 @@ export class PointingCropHandle extends StateNode {
|
|||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setCroppingShapeId(null)
|
||||
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']
|
||||
|
||||
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 = () => {
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.setHintingShapeIds([])
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
|
|
|
@ -28,7 +28,10 @@ export class PointingRotateHandle extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
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 = () => {
|
||||
|
|
|
@ -56,13 +56,16 @@ export class Resizing extends StateNode {
|
|||
this.creationCursorOffset = creationCursorOffset
|
||||
|
||||
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.markId = isCreating
|
||||
? `creating:${this.editor.onlySelectedShape!.id}`
|
||||
: this.editor.mark('starting resizing')
|
||||
|
||||
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'starting resizing'
|
||||
if (!isCreating) this.editor.mark(this.markId)
|
||||
|
||||
this.handleResizeStart()
|
||||
this.updateShapes()
|
||||
|
@ -105,7 +108,7 @@ export class Resizing extends StateNode {
|
|||
this.handleResizeEnd()
|
||||
|
||||
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.root.current.value!.transition('editing_shape', {})
|
||||
return
|
||||
|
@ -349,12 +352,15 @@ export class Resizing extends StateNode {
|
|||
|
||||
nextCursor.rotation = rotation
|
||||
|
||||
this.editor.updateInstanceState({ cursor: nextCursor })
|
||||
this.editor.updateInstanceState({ cursor: nextCursor }, { ephemeral: true, squashing: true })
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ export class Rotating extends StateNode {
|
|||
this.info = info
|
||||
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 })
|
||||
if (!snapshot) return this.parent.transition('idle', this.info)
|
||||
|
@ -40,7 +41,10 @@ export class Rotating extends StateNode {
|
|||
}
|
||||
|
||||
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.snapshot = {} as TLRotationSnapshot
|
||||
|
|
|
@ -108,7 +108,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
private updateScribbleSelection(addPoint: boolean) {
|
||||
const {
|
||||
zoomLevel,
|
||||
shapesOnCurrentPage,
|
||||
currentPageShapes,
|
||||
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
||||
} = this.editor
|
||||
|
||||
|
@ -118,7 +118,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
this.pushPointToScribble()
|
||||
}
|
||||
|
||||
const shapes = shapesOnCurrentPage
|
||||
const shapes = currentPageShapes
|
||||
let shape: TLShape, geometry: Geometry2d, A: Vec2d, B: Vec2d
|
||||
|
||||
for (let i = 0, n = shapes.length; i < n; i++) {
|
||||
|
|
|
@ -31,6 +31,7 @@ export class Translating extends StateNode {
|
|||
snapshot: TranslatingSnapshot = {} as any
|
||||
|
||||
markId = ''
|
||||
initialMarkId = ''
|
||||
|
||||
isCloning = false
|
||||
isCreating = false
|
||||
|
@ -53,9 +54,12 @@ export class Translating extends StateNode {
|
|||
this.isCreating = isCreating
|
||||
this.editAfterComplete = editAfterComplete
|
||||
|
||||
this.markId = isCreating
|
||||
? this.editor.mark(`creating:${this.editor.onlySelectedShape!.id}`)
|
||||
: this.editor.mark('translating')
|
||||
this.initialMarkId = isCreating
|
||||
? `creating:${this.editor.onlySelectedShape!.id}`
|
||||
: 'translating'
|
||||
this.markId = this.initialMarkId
|
||||
if (!isCreating) this.editor.mark(this.markId)
|
||||
|
||||
this.handleEnter(info)
|
||||
this.editor.on('tick', this.updateParent)
|
||||
}
|
||||
|
@ -66,7 +70,10 @@ export class Translating extends StateNode {
|
|||
this.selectionSnapshot = {} as any
|
||||
this.snapshot = {} as any
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -111,7 +118,9 @@ export class Translating extends StateNode {
|
|||
|
||||
this.isCloning = true
|
||||
this.reset()
|
||||
this.markId = this.editor.mark('translating')
|
||||
|
||||
this.markId = 'cloning'
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.duplicateShapes(Array.from(this.editor.selectedShapeIds))
|
||||
|
||||
|
@ -124,7 +133,10 @@ export class Translating extends StateNode {
|
|||
this.isCloning = false
|
||||
this.snapshot = this.selectionSnapshot
|
||||
this.reset()
|
||||
this.markId = this.editor.mark('translating')
|
||||
|
||||
this.markId = this.initialMarkId
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.updateShapes()
|
||||
}
|
||||
|
||||
|
@ -148,7 +160,7 @@ export class Translating extends StateNode {
|
|||
if (this.editAfterComplete) {
|
||||
const onlySelected = this.editor.onlySelectedShape
|
||||
if (onlySelected) {
|
||||
this.editor.setEditingId(onlySelected.id)
|
||||
this.editor.setEditingShapeId(onlySelected.id)
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.root.current.value!.transition('editing_shape', {})
|
||||
}
|
||||
|
@ -171,7 +183,10 @@ export class Translating extends StateNode {
|
|||
this.isCloning = false
|
||||
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)
|
||||
|
||||
// Don't clone on create; otherwise clone on altKey
|
||||
|
@ -201,7 +216,7 @@ export class Translating extends StateNode {
|
|||
})
|
||||
|
||||
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.editor.updateInstanceState(
|
||||
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
|
||||
true
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
this.currentToolIdMask = undefined
|
||||
}
|
||||
|
@ -53,9 +53,15 @@ export class ZoomTool extends StateNode {
|
|||
|
||||
private updateCursor() {
|
||||
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 {
|
||||
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 {
|
||||
const zoomLevel = this.editor.inputs.altKey ? this.editor.zoomLevel / 2 : undefined
|
||||
this.editor.zoomToBounds(
|
||||
zoomBrush.x,
|
||||
zoomBrush.y,
|
||||
zoomBrush.width,
|
||||
zoomBrush.height,
|
||||
zoomLevel,
|
||||
{ duration: 220 }
|
||||
)
|
||||
this.editor.zoomToBounds(zoomBrush, zoomLevel, { duration: 220 })
|
||||
}
|
||||
|
||||
this.parent.transition('idle', this.info)
|
||||
|
|
|
@ -7,7 +7,7 @@ export function updateHoveredId(editor: Editor) {
|
|||
margin: HIT_TEST_MARGIN / editor.zoomLevel,
|
||||
})
|
||||
|
||||
if (!hitShape) return editor.setHoveredId(null)
|
||||
if (!hitShape) return editor.setHoveredShapeId(null)
|
||||
|
||||
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
|
||||
// are ALSO in the viewport.
|
||||
const visibleShapes = renderingShapes.filter((s) => s.isInViewport)
|
||||
const showBackToContentNow =
|
||||
visibleShapes.length === 0 && editor.shapesOnCurrentPage.length > 0
|
||||
const showBackToContentNow = visibleShapes.length === 0 && editor.currentPageShapes.length > 0
|
||||
|
||||
if (showBackToContentPrev !== showBackToContentNow) {
|
||||
setShowBackToContent(showBackToContentNow)
|
||||
|
|
|
@ -7,7 +7,7 @@ export const HTMLCanvas = track(function HTMLCanvas() {
|
|||
const rCanvas = React.useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const camera = editor.camera
|
||||
const shapes = editor.shapesOnCurrentPage
|
||||
const shapes = editor.currentPageShapes
|
||||
if (rCanvas.current) {
|
||||
const cvs = rCanvas.current
|
||||
const ctx = cvs.getContext('2d')!
|
||||
|
|
|
@ -54,16 +54,15 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
|
||||
const onDoubleClick = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!editor.shapeIdsOnCurrentPage.size) return
|
||||
|
||||
const { x, y } = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
if (!editor.currentPageShapeIds.size) return
|
||||
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
|
||||
|
||||
minimap.originPagePoint.setTo(clampedPoint)
|
||||
minimap.originPageCenter.setTo(editor.viewportPageBounds.center)
|
||||
|
||||
editor.centerOnPoint(x, y, { duration: ANIMATION_MEDIUM_MS })
|
||||
editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
[editor, minimap]
|
||||
)
|
||||
|
@ -71,14 +70,13 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
const onPointerDown = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
setPointerCapture(e.currentTarget, e)
|
||||
if (!editor.shapeIdsOnCurrentPage.size) return
|
||||
if (!editor.currentPageShapeIds.size) return
|
||||
|
||||
rPointing.current = true
|
||||
|
||||
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 _vpPageBounds = editor.viewportPageBounds
|
||||
|
@ -89,7 +87,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
||||
|
||||
if (!minimap.isInViewport) {
|
||||
editor.centerOnPoint(x, y, { duration: ANIMATION_MEDIUM_MS })
|
||||
editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS })
|
||||
}
|
||||
},
|
||||
[editor, minimap]
|
||||
|
@ -98,26 +96,21 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
const onPointerMove = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (rPointing.current) {
|
||||
const { x, y } = minimap.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
e.shiftKey,
|
||||
true
|
||||
)
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, e.shiftKey, true)
|
||||
|
||||
if (minimap.isInViewport) {
|
||||
const delta = Vec2d.Sub({ x, y }, minimap.originPagePoint)
|
||||
const delta = Vec2d.Sub(point, minimap.originPagePoint)
|
||||
const center = Vec2d.Add(minimap.originPageCenter, delta)
|
||||
editor.centerOnPoint(center.x, center.y)
|
||||
editor.centerOnPoint(center)
|
||||
return
|
||||
}
|
||||
|
||||
editor.centerOnPoint(x, y)
|
||||
editor.centerOnPoint(point)
|
||||
}
|
||||
|
||||
const pagePoint = minimap.getPagePoint(e.clientX, e.clientY)
|
||||
|
||||
const screenPoint = editor.pageToScreen(pagePoint.x, pagePoint.y)
|
||||
const screenPoint = editor.pageToScreen(pagePoint)
|
||||
|
||||
const info: TLPointerEventInfo = {
|
||||
type: 'pointer',
|
||||
|
@ -178,9 +171,10 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
useQuickReactor(
|
||||
'minimap render when pagebounds or collaborators changes',
|
||||
() => {
|
||||
const { shapeIdsOnCurrentPage, viewportPageBounds, commonBoundsOfAllShapesOnCurrentPage } =
|
||||
const { currentPageShapeIds, viewportPageBounds, commonBoundsOfAllShapesOnCurrentPage } =
|
||||
editor
|
||||
|
||||
// deref
|
||||
const _dpr = devicePixelRatio.value
|
||||
|
||||
minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
|
||||
|
@ -193,7 +187,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
|
||||
const allShapeBounds = [] as (Box2d & { id: TLShapeId })[]
|
||||
|
||||
shapeIdsOnCurrentPage.forEach((id) => {
|
||||
currentPageShapeIds.forEach((id) => {
|
||||
let pageBounds = editor.getPageBounds(id) as Box2d & { id: TLShapeId }
|
||||
if (!pageBounds) return
|
||||
|
||||
|
@ -217,7 +211,7 @@ export function Minimap({ shapeFill, selectFill, viewportFill }: MinimapProps) {
|
|||
minimap.collaborators = presences.value
|
||||
minimap.render()
|
||||
},
|
||||
[editor, minimap]
|
||||
[editor, minimap, devicePixelRatio]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,7 +12,7 @@ export const ZoomMenu = track(function ZoomMenu() {
|
|||
const breakpoint = useBreakpoint()
|
||||
|
||||
const zoom = editor.zoomLevel
|
||||
const hasShapes = editor.shapeIdsOnCurrentPage.size > 0
|
||||
const hasShapes = editor.currentPageShapeIds.size > 0
|
||||
const hasSelected = editor.selectedShapeIds.length > 0
|
||||
const isZoomedTo100 = editor.zoomLevel === 1
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export const PageItemInput = function PageItemInput({
|
|||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
editor.renamePage(id, value ? value : 'New Page', true)
|
||||
editor.updatePage({ id, name: value ? value : 'New Page' }, true)
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ export const PageItemInput = function PageItemInput({
|
|||
const handleComplete = useCallback(
|
||||
(value: string) => {
|
||||
editor.mark('rename page')
|
||||
editor.renamePage(id, value || 'New Page', false)
|
||||
editor.updatePage({ id, name: value || 'New Page' }, false)
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
|
|
@ -326,7 +326,7 @@ export const PageMenu = function PageMenu() {
|
|||
onClick={() => {
|
||||
const name = window.prompt('Rename page', page.name)
|
||||
if (name && name !== page.name) {
|
||||
editor.renamePage(page.id, name)
|
||||
editor.updatePage({ id: page.id, name }, true)
|
||||
}
|
||||
}}
|
||||
onDoubleClick={toggleEditing}
|
||||
|
@ -379,10 +379,10 @@ export const PageMenu = function PageMenu() {
|
|||
item={page}
|
||||
listSize={pages.length}
|
||||
onRename={() => {
|
||||
if (editor.isIos) {
|
||||
if (editor.environment.isIos) {
|
||||
const name = window.prompt('Rename page', page.name)
|
||||
if (name && name !== page.name) {
|
||||
editor.renamePage(page.id, name)
|
||||
editor.updatePage({ id: page.id, name }, true)
|
||||
}
|
||||
} else {
|
||||
setIsEditing(true)
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
SharedStyleMap,
|
||||
StyleProp,
|
||||
minBy,
|
||||
useComputed,
|
||||
useEditor,
|
||||
useValue,
|
||||
} from '@tldraw/editor'
|
||||
|
@ -33,9 +34,7 @@ interface StylePanelProps {
|
|||
}
|
||||
|
||||
const selectToolStyles = [DefaultColorStyle, DefaultDashStyle, DefaultFillStyle, DefaultSizeStyle]
|
||||
function getRelevantStyles(
|
||||
editor: Editor
|
||||
): { styles: ReadonlySharedStyleMap; opacity: SharedStyle<number> } | null {
|
||||
function getRelevantStyles(editor: Editor): ReadonlySharedStyleMap | null {
|
||||
const styles = new SharedStyleMap(editor.sharedStyles)
|
||||
const hasShape = editor.selectedShapeIds.length > 0 || !!editor.root.current.value?.shapeType
|
||||
|
||||
|
@ -46,14 +45,38 @@ function getRelevantStyles(
|
|||
}
|
||||
|
||||
if (styles.size === 0 && !hasShape) return null
|
||||
return { styles, opacity: editor.sharedOpacity }
|
||||
return styles
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
|
||||
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 opacity = useValue('opacity', () => sharedOpacity.value, [sharedOpacity])
|
||||
|
||||
const handlePointerOut = useCallback(() => {
|
||||
if (!isMobile) {
|
||||
|
@ -63,7 +86,7 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
|
|||
|
||||
if (!relevantStyles) return null
|
||||
|
||||
const { styles, opacity } = relevantStyles
|
||||
const styles = relevantStyles
|
||||
const geo = styles.get(GeoShapeGeoStyle)
|
||||
const arrowheadEnd = styles.get(ArrowShapeArrowheadEndStyle)
|
||||
const arrowheadStart = styles.get(ArrowShapeArrowheadStartStyle)
|
||||
|
@ -95,8 +118,8 @@ function useStyleChangeCallback() {
|
|||
|
||||
return React.useMemo(() => {
|
||||
return function <T>(style: StyleProp<T>, value: T, squashing: boolean) {
|
||||
editor.setStyle(style, value, squashing)
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
editor.setStyle(style, value, { ephemeral: false, squashing })
|
||||
editor.updateInstanceState({ isChangingStyle: true }, { ephemeral: true, squashing: true })
|
||||
}
|
||||
}, [editor])
|
||||
}
|
||||
|
@ -118,8 +141,16 @@ function CommonStylePickerSet({
|
|||
const handleOpacityValueChange = React.useCallback(
|
||||
(value: number, ephemeral: boolean) => {
|
||||
const item = tldrawSupportedOpacities[value]
|
||||
editor.setOpacity(item, ephemeral)
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
editor.batch(() => {
|
||||
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]
|
||||
)
|
||||
|
|
|
@ -704,7 +704,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
{
|
||||
id: 'delete',
|
||||
label: 'action.delete',
|
||||
kbd: '⌫,del,backspace',
|
||||
kbd: '⌫,del', // removed backspace; it was firing twice on mac
|
||||
icon: 'trash',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
|
@ -838,10 +838,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
onSelect(source) {
|
||||
trackEvent('toggle-transparent', { source })
|
||||
editor.updateInstanceState(
|
||||
{
|
||||
exportBackground: !editor.instanceState.exportBackground,
|
||||
},
|
||||
true
|
||||
{ exportBackground: !editor.instanceState.exportBackground },
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
},
|
||||
checkbox: true,
|
||||
|
@ -901,7 +899,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
{
|
||||
isDebugMode: !editor.instanceState.isDebugMode,
|
||||
},
|
||||
true
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
},
|
||||
checkbox: true,
|
||||
|
|
|
@ -93,7 +93,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem
|
|||
const threeStackableItems = useThreeStackableItems()
|
||||
const atLeastOneShapeOnPage = useValue(
|
||||
'atLeastOneShapeOnPage',
|
||||
() => editor.shapeIdsOnCurrentPage.size > 0,
|
||||
() => editor.currentPageShapeIds.size > 0,
|
||||
[]
|
||||
)
|
||||
const isTransparentBg = useValue(
|
||||
|
|
|
@ -20,7 +20,7 @@ export function useCopyAs() {
|
|||
// little awkward.
|
||||
function copyAs(ids: TLShapeId[] = editor.selectedShapeIds, format: TLCopyType = 'svg') {
|
||||
if (ids.length === 0) {
|
||||
ids = [...editor.shapeIdsOnCurrentPage]
|
||||
ids = [...editor.currentPageShapeIds]
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
|
|
|
@ -21,7 +21,7 @@ export function useExportAs() {
|
|||
format: TLExportType = 'png'
|
||||
) {
|
||||
if (ids.length === 0) {
|
||||
ids = [...editor.shapeIdsOnCurrentPage]
|
||||
ids = [...editor.currentPageShapeIds]
|
||||
}
|
||||
|
||||
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 { useEffect } from 'react'
|
||||
import { useActions } from './useActions'
|
||||
|
@ -26,12 +26,12 @@ export function useKeyboardShortcuts() {
|
|||
useEffect(() => {
|
||||
if (!isFocused) return
|
||||
|
||||
const scope = uniqueId()
|
||||
|
||||
const container = editor.getContainer()
|
||||
|
||||
hotkeys.setScope(editor.store.id)
|
||||
|
||||
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.
|
||||
|
@ -65,8 +65,11 @@ export function useKeyboardShortcuts() {
|
|||
})
|
||||
}
|
||||
|
||||
// When focused, hotkeys should only respond to kbds in this scope
|
||||
hotkeys.setScope(scope)
|
||||
|
||||
return () => {
|
||||
hotkeys.deleteScope(editor.store.id)
|
||||
hotkeys.deleteScope(scope)
|
||||
}
|
||||
}, [actions, tools, isReadonly, editor, isFocused])
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
|
|||
[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 noneSelected = selectedCount === 0
|
||||
|
|
|
@ -156,10 +156,10 @@ export function usePrint() {
|
|||
}
|
||||
|
||||
function triggerPrint() {
|
||||
if (editor.isChromeForIos) {
|
||||
if (editor.environment.isChromeForIos) {
|
||||
beforePrintHandler()
|
||||
window.print()
|
||||
} else if (editor.isSafari) {
|
||||
} else if (editor.environment.isSafari) {
|
||||
beforePrintHandler()
|
||||
document.execCommand('print', false)
|
||||
} else {
|
||||
|
|
|
@ -111,7 +111,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
|
|||
[GeoShapeGeoStyle.id]: id,
|
||||
},
|
||||
},
|
||||
true
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
editor.setCurrentTool('geo')
|
||||
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
|
||||
|
@ -405,7 +405,7 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
|
|||
}
|
||||
|
||||
// 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
|
||||
const { viewportPageBounds } = editor
|
||||
|
|
|
@ -593,7 +593,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
|
||||
const bounds = editor.commonBoundsOfAllShapesOnCurrentPage
|
||||
if (bounds) {
|
||||
editor.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1)
|
||||
editor.zoomToBounds(bounds, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -293,7 +293,7 @@ export async function parseAndLoadDocument(
|
|||
|
||||
const bounds = editor.commonBoundsOfAllShapesOnCurrentPage
|
||||
if (bounds) {
|
||||
editor.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1)
|
||||
editor.zoomToBounds(bounds, 1)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { BaseBoxShapeUtil, PageRecordType, TLShape, createShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from './TestEditor'
|
||||
import { TL } from './test-jsx'
|
||||
|
||||
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", () => {
|
||||
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])
|
||||
moveShapesToPage2()
|
||||
expect(editor.hintingShapeIds).toEqual([])
|
||||
})
|
||||
test('[frame that does not move]', () => {
|
||||
editor.setHintingIds([ids.frame1])
|
||||
editor.setHintingShapeIds([ids.frame1])
|
||||
expect(editor.hintingShapeIds).toEqual([ids.frame1])
|
||||
moveShapesToPage2()
|
||||
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", () => {
|
||||
test('[root shape]', () => {
|
||||
editor.setEditingId(ids.box1)
|
||||
editor.setEditingShapeId(ids.box1)
|
||||
expect(editor.editingShapeId).toBe(ids.box1)
|
||||
moveShapesToPage2()
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
})
|
||||
test('[child of frame]', () => {
|
||||
editor.setEditingId(ids.box2)
|
||||
editor.setEditingShapeId(ids.box2)
|
||||
expect(editor.editingShapeId).toBe(ids.box2)
|
||||
moveShapesToPage2()
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
})
|
||||
test('[child of group]', () => {
|
||||
editor.setEditingId(ids.box3)
|
||||
editor.setEditingShapeId(ids.box3)
|
||||
expect(editor.editingShapeId).toBe(ids.box3)
|
||||
moveShapesToPage2()
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
})
|
||||
test('[frame that doesnt move]', () => {
|
||||
editor.setEditingId(ids.frame1)
|
||||
editor.setEditingShapeId(ids.frame1)
|
||||
expect(editor.editingShapeId).toBe(ids.frame1)
|
||||
moveShapesToPage2()
|
||||
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", () => {
|
||||
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])
|
||||
moveShapesToPage2()
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
})
|
||||
test('[frame that does not move]', () => {
|
||||
editor.setErasingIds([ids.frame1])
|
||||
editor.setErasingShapeIds([ids.frame1])
|
||||
expect(editor.erasingShapeIds).toEqual([ids.frame1])
|
||||
moveShapesToPage2()
|
||||
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)
|
||||
})
|
||||
|
||||
describe('Editor.sharedOpacity', () => {
|
||||
it('should return the current opacity', () => {
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 1 })
|
||||
editor.setOpacity(0.5)
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.5 })
|
||||
})
|
||||
// describe('Editor.sharedOpacity', () => {
|
||||
// it('should return the current opacity', () => {
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 1 })
|
||||
// editor.setOpacity(0.5)
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.5 })
|
||||
// })
|
||||
|
||||
it('should return opacity for a single selected shape', () => {
|
||||
const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
|
||||
editor.setSelectedShapeIds([A])
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
})
|
||||
// it('should return opacity for a single selected shape', () => {
|
||||
// const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
|
||||
// editor.setSelectedShapeIds([A])
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
// })
|
||||
|
||||
it('should return opacity for multiple selected shapes', () => {
|
||||
const { A, B } = editor.createShapesFromJsx([
|
||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
<TL.geo ref="B" opacity={0.3} x={0} y={0} />,
|
||||
])
|
||||
editor.setSelectedShapeIds([A, B])
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
})
|
||||
// it('should return opacity for multiple selected shapes', () => {
|
||||
// const { A, B } = editor.createShapesFromJsx([
|
||||
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
// <TL.geo ref="B" opacity={0.3} x={0} y={0} />,
|
||||
// ])
|
||||
// editor.setSelectedShapeIds([A, B])
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
// })
|
||||
|
||||
it('should return mixed when multiple selected shapes have different opacity', () => {
|
||||
const { A, B } = editor.createShapesFromJsx([
|
||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
<TL.geo ref="B" opacity={0.5} x={0} y={0} />,
|
||||
])
|
||||
editor.setSelectedShapeIds([A, B])
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
|
||||
})
|
||||
// it('should return mixed when multiple selected shapes have different opacity', () => {
|
||||
// const { A, B } = editor.createShapesFromJsx([
|
||||
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
// <TL.geo ref="B" opacity={0.5} x={0} y={0} />,
|
||||
// ])
|
||||
// editor.setSelectedShapeIds([A, B])
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
|
||||
// })
|
||||
|
||||
it('ignores the opacity of groups and returns the opacity of their children', () => {
|
||||
const ids = editor.createShapesFromJsx([
|
||||
<TL.group ref="group" x={0} y={0}>
|
||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />
|
||||
</TL.group>,
|
||||
])
|
||||
editor.setSelectedShapeIds([ids.group])
|
||||
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
})
|
||||
})
|
||||
// it('ignores the opacity of groups and returns the opacity of their children', () => {
|
||||
// const ids = editor.createShapesFromJsx([
|
||||
// <TL.group ref="group" x={0} y={0}>
|
||||
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />
|
||||
// </TL.group>,
|
||||
// ])
|
||||
// editor.setSelectedShapeIds([ids.group])
|
||||
// expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('Editor.setOpacity', () => {
|
||||
it('should set opacity for selected shapes', () => {
|
||||
const ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
<TL.geo ref="B" opacity={0.4} x={0} y={0} />,
|
||||
])
|
||||
// it('should set opacity for selected shapes', () => {
|
||||
// const ids = editor.createShapesFromJsx([
|
||||
// <TL.geo ref="A" opacity={0.3} x={0} y={0} />,
|
||||
// <TL.geo ref="B" opacity={0.4} x={0} y={0} />,
|
||||
// ])
|
||||
|
||||
editor.setSelectedShapeIds([ids.A, ids.B])
|
||||
editor.setOpacity(0.5)
|
||||
// editor.setSelectedShapeIds([ids.A, ids.B])
|
||||
// editor.setOpacity(0.5)
|
||||
|
||||
expect(editor.getShape(ids.A)!.opacity).toBe(0.5)
|
||||
expect(editor.getShape(ids.B)!.opacity).toBe(0.5)
|
||||
})
|
||||
// expect(editor.getShape(ids.A)!.opacity).toBe(0.5)
|
||||
// expect(editor.getShape(ids.B)!.opacity).toBe(0.5)
|
||||
// })
|
||||
|
||||
it('should traverse into groups and set opacity in their children', () => {
|
||||
const ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="boxA" x={0} y={0} />,
|
||||
<TL.group ref="groupA" x={0} y={0}>
|
||||
<TL.geo ref="boxB" x={0} y={0} />
|
||||
<TL.group ref="groupB" x={0} y={0}>
|
||||
<TL.geo ref="boxC" x={0} y={0} />
|
||||
<TL.geo ref="boxD" x={0} y={0} />
|
||||
</TL.group>
|
||||
</TL.group>,
|
||||
])
|
||||
// it('should traverse into groups and set opacity in their children', () => {
|
||||
// const ids = editor.createShapesFromJsx([
|
||||
// <TL.geo ref="boxA" x={0} y={0} />,
|
||||
// <TL.group ref="groupA" x={0} y={0}>
|
||||
// <TL.geo ref="boxB" x={0} y={0} />
|
||||
// <TL.group ref="groupB" x={0} y={0}>
|
||||
// <TL.geo ref="boxC" x={0} y={0} />
|
||||
// <TL.geo ref="boxD" x={0} y={0} />
|
||||
// </TL.group>
|
||||
// </TL.group>,
|
||||
// ])
|
||||
|
||||
editor.setSelectedShapeIds([ids.groupA])
|
||||
editor.setOpacity(0.5)
|
||||
// editor.setSelectedShapeIds([ids.groupA])
|
||||
// editor.setOpacity(0.5)
|
||||
|
||||
// a wasn't selected...
|
||||
expect(editor.getShape(ids.boxA)!.opacity).toBe(1)
|
||||
// // a wasn't selected...
|
||||
// expect(editor.getShape(ids.boxA)!.opacity).toBe(1)
|
||||
|
||||
// b, c, & d were within a selected group...
|
||||
expect(editor.getShape(ids.boxB)!.opacity).toBe(0.5)
|
||||
expect(editor.getShape(ids.boxC)!.opacity).toBe(0.5)
|
||||
expect(editor.getShape(ids.boxD)!.opacity).toBe(0.5)
|
||||
// // b, c, & d were within a selected group...
|
||||
// expect(editor.getShape(ids.boxB)!.opacity).toBe(0.5)
|
||||
// expect(editor.getShape(ids.boxC)!.opacity).toBe(0.5)
|
||||
// expect(editor.getShape(ids.boxD)!.opacity).toBe(0.5)
|
||||
|
||||
// groups get skipped
|
||||
expect(editor.getShape(ids.groupA)!.opacity).toBe(1)
|
||||
expect(editor.getShape(ids.groupB)!.opacity).toBe(1)
|
||||
})
|
||||
// // groups get skipped
|
||||
// expect(editor.getShape(ids.groupA)!.opacity).toBe(1)
|
||||
// expect(editor.getShape(ids.groupB)!.opacity).toBe(1)
|
||||
// })
|
||||
|
||||
it('stores opacity on opacityForNextShape', () => {
|
||||
editor.setOpacity(0.5)
|
||||
|
|
|
@ -99,7 +99,7 @@ describe('When clicking', () => {
|
|||
// Starts in idle
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(0, 0) // near enough to box1
|
||||
|
||||
|
@ -108,11 +108,10 @@ describe('When clicking', () => {
|
|||
|
||||
// Sets the erasingShapeIds array / erasingShapeIdsSet
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
|
||||
// Deletes the erasing shapes
|
||||
expect(editor.getShape(ids.box1)).toBeUndefined()
|
||||
|
@ -120,7 +119,6 @@ describe('When clicking', () => {
|
|||
|
||||
// Also empties the erasingShapeIds array / erasingShapeIdsSet
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
|
||||
// Returns to idle
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
@ -128,30 +126,29 @@ describe('When clicking', () => {
|
|||
editor.undo()
|
||||
|
||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(shapesBeforeCount)
|
||||
expect(editor.currentPageShapes.length).toBe(shapesBeforeCount)
|
||||
|
||||
editor.redo()
|
||||
|
||||
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', () => {
|
||||
editor.setCurrentTool('eraser')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(99, 99) // next to box1 AND in 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()
|
||||
|
||||
expect(editor.getShape(ids.box1)).toBeUndefined()
|
||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount - 2)
|
||||
})
|
||||
|
||||
|
@ -159,16 +156,15 @@ describe('When clicking', () => {
|
|||
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
||||
editor.setCurrentTool('eraser')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(350, 350) // in box3
|
||||
|
||||
expect(new Set(editor.erasingShapeIds)).toEqual(new Set([ids.group1]))
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.group1]))
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
|
||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||
expect(editor.getShape(ids.box3)).toBeUndefined()
|
||||
|
@ -181,28 +177,26 @@ describe('When clicking', () => {
|
|||
editor.groupShapes([ids.box2, ids.box3], ids.group1)
|
||||
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
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||
})
|
||||
|
||||
it('Stops erasing when it reaches a frame when the frame was not was the top-most hovered shape', () => {
|
||||
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
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box4]))
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount - 1)
|
||||
|
||||
// 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', () => {
|
||||
editor.setCurrentTool('eraser')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(325, 25) // directly on frame1, not its children
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
|
||||
editor.pointerUp() // without dragging!
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||
|
||||
// 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', () => {
|
||||
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
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
|
||||
editor.pointerUp() // without dragging!
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||
|
||||
// Erases NEITHER the frame nor its child
|
||||
|
@ -250,25 +242,23 @@ describe('When clicking', () => {
|
|||
editor.setCurrentTool('eraser')
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(0, 0) // in box1
|
||||
editor.expectPathToBe('root.eraser.pointing')
|
||||
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
|
||||
editor.cancel()
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
||||
// Does NOT erase the shape
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||
})
|
||||
|
@ -277,25 +267,23 @@ describe('When clicking', () => {
|
|||
editor.setCurrentTool('eraser')
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
||||
const shapesBeforeCount = editor.shapesOnCurrentPage.length
|
||||
const shapesBeforeCount = editor.currentPageShapes.length
|
||||
|
||||
editor.pointerDown(0, 0) // near to box1
|
||||
editor.expectPathToBe('root.eraser.pointing')
|
||||
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
|
||||
editor.interrupt()
|
||||
|
||||
editor.pointerUp()
|
||||
|
||||
const shapesAfterCount = editor.shapesOnCurrentPage.length
|
||||
const shapesAfterCount = editor.currentPageShapes.length
|
||||
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
|
||||
// Does NOT erase the shape
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
expect(shapesAfterCount).toBe(shapesBeforeCount)
|
||||
})
|
||||
|
@ -320,24 +308,20 @@ describe('When clicking and dragging', () => {
|
|||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
|
||||
// editor.pointerUp()
|
||||
// editor.expectPathToBe('root.eraser.idle')
|
||||
// expect(editor.erasingShapeIds).toEqual([])
|
||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
||||
|
||||
// editor.undo()
|
||||
|
||||
// expect(editor.erasingShapeIds).toEqual([])
|
||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
// expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
|
||||
// editor.redo()
|
||||
|
||||
// expect(editor.erasingShapeIds).toEqual([])
|
||||
// expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
// expect(editor.getShape(ids.box1)).not.toBeDefined()
|
||||
})
|
||||
|
||||
|
@ -349,11 +333,9 @@ describe('When clicking and dragging', () => {
|
|||
jest.advanceTimersByTime(16)
|
||||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
editor.cancel()
|
||||
editor.expectPathToBe('root.eraser.idle')
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
})
|
||||
|
||||
|
@ -366,10 +348,8 @@ describe('When clicking and dragging', () => {
|
|||
jest.advanceTimersByTime(16)
|
||||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
editor.pointerMove(0, 0)
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box1])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box1]))
|
||||
expect(editor.getShape(ids.box1)).toBeDefined()
|
||||
editor.pointerUp()
|
||||
expect(editor.getShape(ids.group1)).toBeDefined()
|
||||
|
@ -383,7 +363,6 @@ describe('When clicking and dragging', () => {
|
|||
jest.advanceTimersByTime(16)
|
||||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box3]))
|
||||
editor.pointerUp()
|
||||
expect(editor.getShape(ids.frame1)).toBeDefined()
|
||||
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
||||
|
@ -397,7 +376,6 @@ describe('When clicking and dragging', () => {
|
|||
jest.advanceTimersByTime(16)
|
||||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([]))
|
||||
editor.pointerUp()
|
||||
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
|
||||
expect(editor.instanceState.scribble).not.toBe(null)
|
||||
expect(editor.erasingShapeIds).toEqual([ids.box3])
|
||||
expect(editor.erasingShapeIdsSet).toEqual(new Set([ids.box3]))
|
||||
editor.pointerUp()
|
||||
expect(editor.getShape(ids.box3)).not.toBeDefined()
|
||||
})
|
||||
|
@ -437,7 +414,7 @@ describe('When clicking and dragging', () => {
|
|||
describe('Does not erase hollow shapes on click', () => {
|
||||
it('Returns to select on cancel', () => {
|
||||
editor.selectAll().deleteShapes(editor.selectedShapes)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.createShape({
|
||||
id: createShapeId(),
|
||||
type: 'geo',
|
||||
|
@ -447,7 +424,7 @@ describe('Does not erase hollow shapes on click', () => {
|
|||
editor.pointerDown()
|
||||
expect(editor.erasingShapeIds).toEqual([])
|
||||
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.pointerMove(200, 200)
|
||||
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
||||
const t1 = [...editor.shapeIdsOnCurrentPage.values()]
|
||||
const t1 = [...editor.currentPageShapeIds.values()]
|
||||
|
||||
editor.keyDown('Alt')
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
||||
// const t2 = [...editor.shapeIds.values()]
|
||||
|
||||
editor.keyUp('Alt')
|
||||
|
||||
// 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
|
||||
|
||||
// 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.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?
|
||||
// editor.keyDown('Alt')
|
||||
// expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
// expect(editor.currentPageShapes.length).toBe(2)
|
||||
// editor.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
||||
// expect([...editor.shapeIds.values()]).toMatchObject(t2)
|
||||
})
|
||||
|
@ -95,7 +95,7 @@ describe('TLSelectTool.Translating', () => {
|
|||
editor.pointerMove(150, 250)
|
||||
editor.pointerUp()
|
||||
const box2Id = editor.onlySelectedShape!.id
|
||||
expect(editor.shapesOnCurrentPage.length).toStrictEqual(2)
|
||||
expect(editor.currentPageShapes.length).toStrictEqual(2)
|
||||
expect(ids.box1).not.toEqual(box2Id)
|
||||
|
||||
// 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])
|
||||
editor.pointerMove(250, 150)
|
||||
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)
|
||||
.selectNone()
|
||||
.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')
|
||||
})
|
||||
})
|
||||
|
@ -358,45 +358,45 @@ describe('When editing shapes', () => {
|
|||
it('Double clicking the canvas creates a new text shape', () => {
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
expect(editor.selectedShapeIds.length).toBe(0)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
||||
expect(editor.currentPageShapes.length).toBe(5)
|
||||
editor.doubleClick(750, 750)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
||||
expect(editor.shapesOnCurrentPage[5].type).toBe('text')
|
||||
expect(editor.currentPageShapes.length).toBe(6)
|
||||
expect(editor.currentPageShapes[5].type).toBe('text')
|
||||
})
|
||||
|
||||
it('It deletes an empty text shape when your click away', () => {
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
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
|
||||
editor.doubleClick(750, 750)
|
||||
expect(editor.selectedShapeIds.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
||||
expect(editor.currentPageShapes.length).toBe(6)
|
||||
const shapeId = editor.selectedShapeIds[0]
|
||||
|
||||
// Click away
|
||||
editor.click(1000, 1000)
|
||||
expect(editor.selectedShapeIds.length).toBe(0)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
||||
expect(editor.currentPageShapes.length).toBe(5)
|
||||
expect(editor.getShape(shapeId)).toBe(undefined)
|
||||
})
|
||||
|
||||
it('It deletes an empty text shape when your click another text shape', () => {
|
||||
expect(editor.editingShapeId).toBe(null)
|
||||
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
|
||||
editor.doubleClick(750, 750)
|
||||
expect(editor.selectedShapeIds.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(6)
|
||||
expect(editor.currentPageShapes.length).toBe(6)
|
||||
const shapeId = editor.selectedShapeIds[0]
|
||||
|
||||
// Click another text shape
|
||||
editor.click(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) })
|
||||
expect(editor.selectedShapeIds.length).toBe(1)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(5)
|
||||
expect(editor.currentPageShapes.length).toBe(5)
|
||||
expect(editor.getShape(shapeId)).toBe(undefined)
|
||||
})
|
||||
|
||||
|
|
|
@ -219,7 +219,10 @@ describe('<TldrawEditor />', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
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()
|
||||
|
@ -340,7 +343,10 @@ describe('Custom shapes', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
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()
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
it('Correctly zooms in when clicking', () => {
|
||||
editor.keyDown('z')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.click()
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
@ -52,7 +52,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
editor.keyDown('z')
|
||||
editor.keyDown('Alt')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.click()
|
||||
jest.advanceTimersByTime(300)
|
||||
|
@ -109,7 +109,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
|
||||
it('When the dragged area is small it zooms in instead of zooming to the area', () => {
|
||||
const originalCenter = { x: 540, y: 360 }
|
||||
const originalPageBounds = { x: 0, y: 0, w: 1080, h: 720 }
|
||||
const originalPageBounds = { x: -0, y: -0, w: 1080, h: 720 }
|
||||
const change = 6
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject(originalPageBounds)
|
||||
|
@ -143,7 +143,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
const newBoundsY = 200
|
||||
editor.expectToBeIn('select.idle')
|
||||
expect(editor.zoomLevel).toBe(1)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.keyDown('z')
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
@ -179,7 +179,7 @@ describe('TLSelectTool.Zooming', () => {
|
|||
editor.expectToBeIn('select.idle')
|
||||
const originalZoomLevel = 1
|
||||
expect(editor.zoomLevel).toBe(originalZoomLevel)
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: 0, y: 0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageBounds).toMatchObject({ x: -0, y: -0, w: 1080, h: 720 })
|
||||
expect(editor.viewportPageCenter).toMatchObject({ x: 540, y: 360 })
|
||||
editor.keyDown('z')
|
||||
editor.expectToBeIn('zoom.idle')
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('Making an arrow on the page', () => {
|
|||
editor.setCurrentTool('arrow')
|
||||
editor.pointerMove(0, 0)
|
||||
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', () => {
|
||||
|
@ -44,24 +44,24 @@ describe('Making an arrow on the page', () => {
|
|||
editor.setCurrentTool('arrow')
|
||||
editor.pointerMove(0, 0)
|
||||
editor.click()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
// with double click
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerMove(0, 0)
|
||||
editor.doubleClick()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
// with pointer up
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
|
||||
// did not add it to the history stack
|
||||
editor.undo()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
editor.redo()
|
||||
editor.redo()
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(0)
|
||||
expect(editor.currentPageShapes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps the arrow if the user dragged', () => {
|
||||
|
@ -75,7 +75,7 @@ describe('Making an arrow on the page', () => {
|
|||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(0, 0)
|
||||
editor.pointerMove(100, 0)
|
||||
const arrow1 = editor.shapesOnCurrentPage[0]
|
||||
const arrow1 = editor.currentPageShapes[0]
|
||||
|
||||
expect(arrow()).toMatchObject({
|
||||
type: 'arrow',
|
||||
|
@ -262,25 +262,25 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
it('does not create the arrow immediately', () => {
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(50, 50)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(arrow()).toBe(null)
|
||||
})
|
||||
|
||||
it('does not create a shape if pointer up before drag', () => {
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(50, 50)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
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', () => {
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(50, 50)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(55, 50)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 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', () => {
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(20, 20) // upper left
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(25, 20)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 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', () => {
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(20, 20) // upper left
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(1)
|
||||
expect(editor.currentPageShapes.length).toBe(1)
|
||||
expect(arrow()).toBe(null)
|
||||
jest.advanceTimersByTime(1000)
|
||||
editor.pointerMove(25, 20)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 20,
|
||||
y: 20,
|
||||
|
@ -383,10 +383,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(25, 25)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(30, 30)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
||||
expect(editor.currentPageShapes.length).toBe(3)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 25,
|
||||
y: 25,
|
||||
|
@ -403,8 +403,8 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
type: 'binding',
|
||||
boundShapeId: ids.box2,
|
||||
normalizedAnchor: {
|
||||
x: 0.6,
|
||||
y: 0.6,
|
||||
x: 0.55,
|
||||
y: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -417,10 +417,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(25, 25)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(30, 30)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
||||
expect(editor.currentPageShapes.length).toBe(3)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 25,
|
||||
y: 25,
|
||||
|
@ -437,8 +437,8 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
type: 'binding',
|
||||
boundShapeId: ids.box2,
|
||||
normalizedAnchor: {
|
||||
x: 0.6,
|
||||
y: 0.6,
|
||||
x: 0.55,
|
||||
y: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -462,10 +462,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(25, 25)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(30, 30)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
||||
expect(editor.currentPageShapes.length).toBe(3)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 25,
|
||||
y: 25,
|
||||
|
@ -498,10 +498,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
|
||||
editor.setCurrentTool('arrow')
|
||||
editor.pointerDown(25, 25)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(2)
|
||||
expect(editor.currentPageShapes.length).toBe(2)
|
||||
expect(arrow()).toBe(null)
|
||||
editor.pointerMove(30, 30)
|
||||
expect(editor.shapesOnCurrentPage.length).toBe(3)
|
||||
expect(editor.currentPageShapes.length).toBe(3)
|
||||
expect(arrow()).toMatchObject({
|
||||
x: 25,
|
||||
y: 25,
|
||||
|
@ -518,11 +518,87 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
|||
type: 'binding',
|
||||
boundShapeId: ids.box2,
|
||||
normalizedAnchor: {
|
||||
x: 0.6,
|
||||
y: 0.6,
|
||||
// kicked over because it was too close to the center, and we can't have both bound there
|
||||
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