diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90b057ee0..866c9ef92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,4 +17,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - test-command: 'yarn test --ci --runInBand' + test-command: 'yarn test --ci --runInBand --updateSnapshot' diff --git a/.gitignore b/.gitignore index 94dd9aa61..83f95d4be 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ coverage www/public/worker-* www/public/sw.js www/public/sw.js.map + +.vercel diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea617610e..0fa7898dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Welcome to the TLDraw contributing guide +# Welcome to the tldraw contributing guide Thank you for investing your time in contributing to our project! Any contribution you make will be reflected in the @tldraw/tldraw package and at [tldraw.com](https://tldraw.com). @@ -77,4 +77,4 @@ When you're finished with the changes, create a pull request, also known as a PR Congratulations :tada::tada: The GitHub team thanks you :sparkles:. -Once your PR is merged, your contributions will become part of the next TLDraw release, and will be visible in the [TLDraw app](https://tldraw.com). +Once your PR is merged, your contributions will become part of the next tldraw release, and will be visible in the [tldraw app](https://tldraw.com). diff --git a/README.md b/README.md index 7aad8b6fc..5bb7c294f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ # @tldraw/tldraw -This package contains the [TLDraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. +This package contains the [tldraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. 💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). 🙌 Questions? Join the [Discord channel](https://discord.gg/SBBEVCA4PG) or start a [discussion](https://github.com/tldraw/tldraw/discussions/new). -🎨 Want to build your own TLDraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). +🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). ## Installation @@ -24,13 +24,13 @@ npm i @tldraw/tldraw ## Usage -Import the `TLDraw` React component and use it in your app. +Import the `tldraw` React component and use it in your app. ```tsx -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' function App() { - return + return } ``` @@ -39,58 +39,58 @@ function App() { You can use the `id` to persist the state in a user's browser storage. ```tsx -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' function App() { - return + return } ``` ### Controlling the Component through Props -You can control the `TLDraw` component through its props. +You can control the `tldraw` component through its props. ```tsx -import { TLDraw, TLDrawDocument } from '@tldraw/tldraw' +import { Tldraw, TDDocument } from '@tldraw/tldraw' function App() { - const myDocument: TLDrawDocument = {} + const myDocument: TDDocument = {} - return + return } ``` -### Controlling the Component through the TLDrawState API +### Controlling the Component through the tldrawApp API -You can also control the `TLDraw` component imperatively through the `TLDrawState` API. +You can also control the `tldraw` component imperatively through the `tldrawApp` API. ```tsx -import { TLDraw, TLDrawState } from '@tldraw/tldraw' +import { Tldraw, tldrawApp } from '@tldraw/tldraw' function App() { - const handleMount = React.useCallback((state: TLDrawState) => { + const handleMount = React.useCallback((state: tldrawApp) => { state.selectAll() }, []) - return + return } ``` -Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section of the [documentation](guides/documentation) for more on this API. +Internally, the `tldraw` component's user interface uses this API to make changes to the component's state. See the `tldrawApp` section of the [documentation](guides/documentation) for more on this API. ### Responding to Changes You can respond to changes and user actions using the `onChange` callback. For more specific changes, you can also use the `onPatch`, `onCommand`, or `onPersist` callbacks. See the [documentation](guides/documentation) for more. ```tsx -import { TLDraw, TLDrawState } from '@tldraw/tldraw' +import { Tldraw, tldrawApp } from '@tldraw/tldraw' function App() { - const handleChange = React.useCallback((state: TLDrawState, reason: string) => { + const handleChange = React.useCallback((state: tldrawApp, reason: string) => { // Do something with the change }, []) - return + return } ``` @@ -108,7 +108,7 @@ See the [development guide](/guides/development.md). ## Example -See the `example` folder for examples of how to use the `` component. +See the `example` folder for examples of how to use the `` component. ## Community diff --git a/electron/README.md b/electron/README.md index f8b5d1945..79ff564a0 100644 --- a/electron/README.md +++ b/electron/README.md @@ -1,6 +1,6 @@ # @tldraw/tldraw-electron -An electron wrapper for TLDraw. Not yet published. +An electron wrapper for tldraw. Not yet published. ## Development diff --git a/electron/src/main/createWindow.ts b/electron/src/main/createWindow.ts index 605c71782..774ef0841 100644 --- a/electron/src/main/createWindow.ts +++ b/electron/src/main/createWindow.ts @@ -13,7 +13,7 @@ export async function createWindow() { minHeight: 480, minWidth: 600, titleBarStyle: 'hidden', - title: 'TLDraw', + title: 'Tldraw', webPreferences: { nodeIntegration: true, devTools: true, diff --git a/electron/src/main/preload.ts b/electron/src/main/preload.ts index 7ecaf57c8..6776e7e5c 100644 --- a/electron/src/main/preload.ts +++ b/electron/src/main/preload.ts @@ -1,7 +1,7 @@ import { contextBridge, ipcRenderer } from 'electron' -import type { Message, TLApi } from 'src/types' +import type { Message, TldrawBridgeApi } from 'src/types' -const api: TLApi = { +const api: TldrawBridgeApi = { send: (channel: string, data: Message) => { ipcRenderer.send(channel, data) }, @@ -10,6 +10,6 @@ const api: TLApi = { }, } -contextBridge?.exposeInMainWorld('TLApi', api) +contextBridge?.exposeInMainWorld('TldrawBridgeApi', api) export {} diff --git a/electron/src/renderer/app.tsx b/electron/src/renderer/app.tsx index e75c79863..d86b50831 100644 --- a/electron/src/renderer/app.tsx +++ b/electron/src/renderer/app.tsx @@ -1,85 +1,86 @@ import * as React from 'react' -import { TLDraw, TLDrawState } from '@tldraw/tldraw' -import type { IpcMainEvent, IpcMain, IpcRenderer } from 'electron' -import type { Message, TLApi } from 'src/types' +import { Tldraw, TldrawApp } from '@tldraw/tldraw' +import type { Message, TldrawBridgeApi } from 'src/types' + +declare const window: Window & { TldrawBridgeApi: TldrawBridgeApi } export default function App(): JSX.Element { - const rTLDrawState = React.useRef() + const rTldrawApp = React.useRef() // When the editor mounts, save the state instance in a ref. - const handleMount = React.useCallback((tldr: TLDrawState) => { - rTLDrawState.current = tldr + const handleMount = React.useCallback((tldr: TldrawApp) => { + rTldrawApp.current = tldr }, []) React.useEffect(() => { function handleEvent(message: Message) { - const state = rTLDrawState.current - if (!state) return + const app = rTldrawApp.current + if (!app) return switch (message.type) { case 'resetZoom': { - state.resetZoom() + app.resetZoom() break } case 'zoomIn': { - state.zoomIn() + app.zoomIn() break } case 'zoomOut': { - state.zoomOut() + app.zoomOut() break } case 'zoomToFit': { - state.zoomToFit() + app.zoomToFit() break } case 'zoomToSelection': { - state.zoomToSelection() + app.zoomToSelection() break } case 'undo': { - state.undo() + app.undo() break } case 'redo': { - state.redo() + app.redo() break } case 'cut': { - state.cut() + app.cut() break } case 'copy': { - state.copy() + app.copy() break } case 'paste': { - state.paste() + app.paste() break } case 'delete': { - state.delete() + app.delete() break } case 'selectAll': { - state.selectAll() + app.selectAll() break } case 'selectNone': { - state.selectNone() + app.selectNone() break } } } - const { send, on } = (window as unknown as Window & { TLApi: TLApi })['TLApi'] + const { on } = window.TldrawBridgeApi on('projectMsg', handleEvent) }) return (
- +
) } diff --git a/electron/src/renderer/decs.d.ts b/electron/src/renderer/decs.d.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/electron/src/types.ts b/electron/src/types.ts index 4a9fddf9a..a26fca561 100644 --- a/electron/src/types.ts +++ b/electron/src/types.ts @@ -13,7 +13,7 @@ export type Message = | { type: 'selectAll' } | { type: 'selectNone' } -export type TLApi = { +export type TldrawBridgeApi = { send: (channel: string, data: Message) => void on: (channel: string, cb: (message: Message) => void) => void } diff --git a/example/package.json b/example/package.json index 8bcda0c45..c41427e24 100644 --- a/example/package.json +++ b/example/package.json @@ -35,5 +35,9 @@ "rimraf": "3.0.2", "typescript": "4.2.3" }, - "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6" + "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6", + "dependencies": { + "@liveblocks/client": "^0.12.3", + "@liveblocks/react": "^0.12.3" + } } diff --git a/example/src/api-control.tsx b/example/src/api-control.tsx index 97fd18cc3..0c48f26b7 100644 --- a/example/src/api-control.tsx +++ b/example/src/api-control.tsx @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { ColorStyle, TLDraw, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw' +import { ColorStyle, Tldraw, TDShapeType, TldrawApp } from '@tldraw/tldraw' export default function Imperative(): JSX.Element { - const rTLDrawState = React.useRef() + const rTldrawApp = React.useRef() - const handleMount = React.useCallback((state: TLDrawState) => { - rTLDrawState.current = state + const handleMount = React.useCallback((app: TldrawApp) => { + rTldrawApp.current = app - state.createShapes( + app.createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, name: 'Rectangle', childIndex: 1, point: [0, 0], @@ -20,7 +20,7 @@ export default function Imperative(): JSX.Element { { id: 'rect2', name: 'Rectangle', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100], } @@ -30,13 +30,13 @@ export default function Imperative(): JSX.Element { React.useEffect(() => { let i = 0 const interval = setInterval(() => { - const state = rTLDrawState.current! - const rect1 = state.getShape('rect1') + const app = rTldrawApp.current! + const rect1 = app.getShape('rect1') if (!rect1) { - state.createShapes({ + app.createShapes({ id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, name: 'Rectangle', childIndex: 1, point: [0, 0], @@ -47,7 +47,7 @@ export default function Imperative(): JSX.Element { const color = i % 2 ? ColorStyle.Red : ColorStyle.Blue - state.patchShapes({ + app.patchShapes({ id: 'rect1', style: { ...rect1.style, @@ -60,5 +60,5 @@ export default function Imperative(): JSX.Element { return () => clearInterval(interval) }, []) - return + return } diff --git a/example/src/api.tsx b/example/src/api.tsx index 86bc9cad0..e1ad11a31 100644 --- a/example/src/api.tsx +++ b/example/src/api.tsx @@ -1,16 +1,20 @@ import * as React from 'react' -import { TLDraw, TLDrawState, TLDrawShapeType, ColorStyle } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, TDShapeType, ColorStyle } from '@tldraw/tldraw' + +declare const window: Window & { app: TldrawApp } export default function Api(): JSX.Element { - const rTLDrawState = React.useRef() + const rTldrawApp = React.useRef() - const handleMount = React.useCallback((state: TLDrawState) => { - rTLDrawState.current = state + const handleMount = React.useCallback((app: TldrawApp) => { + rTldrawApp.current = app - state + window.app = app + + app .createShapes({ id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [100, 100], size: [200, 200], }) @@ -24,7 +28,7 @@ export default function Api(): JSX.Element { return (
- +
) } diff --git a/example/src/app.tsx b/example/src/app.tsx index b1f03477a..29b69f07d 100644 --- a/example/src/app.tsx +++ b/example/src/app.tsx @@ -19,9 +19,8 @@ import './styles.css' export default function App(): JSX.Element { return (
- - + @@ -64,9 +63,10 @@ export default function App(): JSX.Element { +
  • - Develop + Develop

  • @@ -94,10 +94,10 @@ export default function App(): JSX.Element { Controlled via Props
  • - Using the TLDrawState API + Using the TldrawApp API
  • - Controlled via TLDrawState API + Controlled via TldrawApp API
  • Changing ID diff --git a/example/src/basic.tsx b/example/src/basic.tsx index 857f8b622..1f244a4c5 100644 --- a/example/src/basic.tsx +++ b/example/src/basic.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' export default function Basic(): JSX.Element { return (
    - +
    ) } diff --git a/example/src/changing-id.tsx b/example/src/changing-id.tsx index 546b6255a..c202391a0 100644 --- a/example/src/changing-id.tsx +++ b/example/src/changing-id.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' export default function ChangingId() { const [id, setId] = React.useState('example') @@ -10,5 +10,5 @@ export default function ChangingId() { return () => clearTimeout(timeout) }, []) - return + return } diff --git a/example/src/develop.tsx b/example/src/develop.tsx index 5a228439b..f54b04830 100644 --- a/example/src/develop.tsx +++ b/example/src/develop.tsx @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react' -import { TLDraw, TLDrawState, useFileSystem } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw' -declare const window: Window & { state: TLDrawState } +declare const window: Window & { app: TldrawApp } export default function Develop(): JSX.Element { - const rTLDrawState = React.useRef() + const rTldrawApp = React.useRef() const fileSystemEvents = useFileSystem() - const handleMount = React.useCallback((state: TLDrawState) => { - window.state = state - rTLDrawState.current = state + const handleMount = React.useCallback((app: TldrawApp) => { + window.app = app + rTldrawApp.current = app }, []) const handleSignOut = React.useCallback(() => { @@ -28,13 +28,14 @@ export default function Develop(): JSX.Element { return (
    -
    ) diff --git a/example/src/embedded.tsx b/example/src/embedded.tsx index 28923e0d3..3eca964e5 100644 --- a/example/src/embedded.tsx +++ b/example/src/embedded.tsx @@ -1,4 +1,4 @@ -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' import * as React from 'react' export default function Embedded(): JSX.Element { @@ -13,7 +13,7 @@ export default function Embedded(): JSX.Element { marginBottom: '32px', }} > - +
    - +
    ) diff --git a/example/src/file-system.tsx b/example/src/file-system.tsx index 2686e095a..14c41dbc8 100644 --- a/example/src/file-system.tsx +++ b/example/src/file-system.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { TLDraw, useFileSystem } from '@tldraw/tldraw' +import { Tldraw, useFileSystem } from '@tldraw/tldraw' export default function FileSystem(): JSX.Element { const fileSystemEvents = useFileSystem() @@ -8,7 +8,7 @@ export default function FileSystem(): JSX.Element { return (
    - +
    ) } diff --git a/example/src/loading-files.tsx b/example/src/loading-files.tsx index 893913657..53263418b 100644 --- a/example/src/loading-files.tsx +++ b/example/src/loading-files.tsx @@ -1,8 +1,8 @@ -import { TLDraw, TLDrawFile } from '@tldraw/tldraw' +import { Tldraw, TDFile } from '@tldraw/tldraw' import * as React from 'react' export default function LoadingFiles(): JSX.Element { - const [file, setFile] = React.useState() + const [file, setFile] = React.useState() React.useEffect(() => { async function loadFile(): Promise { @@ -13,5 +13,5 @@ export default function LoadingFiles(): JSX.Element { loadFile() }, []) - return + return } diff --git a/example/src/multiplayer/multiplayer.tsx b/example/src/multiplayer/multiplayer.tsx index 443faa88d..66def6778 100644 --- a/example/src/multiplayer/multiplayer.tsx +++ b/example/src/multiplayer/multiplayer.tsx @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, TDDocument, TDUser } from '@tldraw/tldraw' import { createClient, Presence } from '@liveblocks/client' import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react' import { Utils } from '@tldraw/core' -interface TLDrawUserPresence extends Presence { - user: TLDrawUser +declare const window: Window & { app: TldrawApp } + +interface TDUserPresence extends Presence { + user: TDUser } const client = createClient({ @@ -20,37 +22,34 @@ export function Multiplayer() { return ( - + ) } -function TLDrawWrapper() { +function TldrawWrapper() { const [docId] = React.useState(() => Utils.uniqueId()) const [error, setError] = React.useState() - const [state, setstate] = React.useState() + const [app, setApp] = React.useState() useErrorListener((err) => setError(err)) - const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', { + const doc = useObject<{ uuid: string; document: TDDocument }>('doc', { uuid: docId, document: { - ...TLDrawState.defaultDocument, + ...TldrawApp.defaultDocument, id: roomId, }, }) // Put the state into the window, for debugging. const handleMount = React.useCallback( - (state: TLDrawState) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window.state = state - state.loadRoom(roomId) - setstate(state) + (app: TldrawApp) => { + window.app = app + setApp(app) }, [roomId] ) @@ -60,17 +59,11 @@ function TLDrawWrapper() { if (!room) return if (!doc) return - if (!state) return - if (!state.state.room) return - - // Update the user's presence with the user from state - const { users, userId } = state.state.room - - room.updatePresence({ id: userId, user: users[userId] }) + if (!app) return // Subscribe to presence changes; when others change, update the state - room.subscribe('others', (others) => { - state.updateUsers( + room.subscribe('others', (others) => { + app.updateUsers( others .toArray() .filter((other) => other.presence) @@ -81,23 +74,23 @@ function TLDrawWrapper() { room.subscribe('event', (event) => { if (event.event?.name === 'exit') { - state.removeUser(event.event.userId) + app.removeUser(event.event.userId) } }) function handleDocumentUpdates() { if (!doc) return - if (!state) return - if (!state.state.room) return + if (!app) return + if (!app.room) return const docObject = doc.toObject() // Only merge the change if it caused by someone else if (docObject.uuid !== docId) { - state.mergeDocument(docObject.document) + app.mergeDocument(docObject.document) } else { - state.updateUsers( - Object.values(state.state.room.users).map((user) => { + app.updateUsers( + Object.values(app.room.users).map((user) => { return { ...user, selectedIds: user.selectedIds, @@ -108,8 +101,8 @@ function TLDrawWrapper() { } function handleExit() { - if (!(state && state.state.room)) return - room?.broadcastEvent({ name: 'exit', userId: state.state.room.userId }) + if (!(app && app.room)) return + room?.broadcastEvent({ name: 'exit', userId: app.room.userId }) } window.addEventListener('beforeunload', handleExit) @@ -121,26 +114,33 @@ function TLDrawWrapper() { const newDocument = doc.toObject().document if (newDocument) { - state.loadDocument(newDocument) + app.loadDocument(newDocument) + app.loadRoom(roomId) + + // Update the user's presence with the user from state + if (app.state.room) { + const { users, userId } = app.state.room + room.updatePresence({ id: userId, user: users[userId] }) + } } return () => { window.removeEventListener('beforeunload', handleExit) doc.unsubscribe(handleDocumentUpdates) } - }, [doc, docId, state]) + }, [doc, docId, app]) const handlePersist = React.useCallback( - (state: TLDrawState) => { - doc?.update({ uuid: docId, document: state.document }) + (app: TldrawApp) => { + doc?.update({ uuid: docId, document: app.document }) }, [docId, doc] ) const handleUserChange = React.useCallback( - (state: TLDrawState, user: TLDrawUser) => { + (app: TldrawApp, user: TDUser) => { const room = client.getRoom(roomId) - room?.updatePresence({ id: state.state.room?.userId, user }) + room?.updatePresence({ id: app.room?.userId, user }) }, [client] ) @@ -151,7 +151,7 @@ function TLDrawWrapper() { return (
    - + return } diff --git a/example/src/persisted.tsx b/example/src/persisted.tsx index a51cf9252..1d0e2ed67 100644 --- a/example/src/persisted.tsx +++ b/example/src/persisted.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' export default function Persisted(): JSX.Element { return (
    - +
    ) } diff --git a/example/src/props-control.tsx b/example/src/props-control.tsx index c31a4f7bf..fe92c60aa 100644 --- a/example/src/props-control.tsx +++ b/example/src/props-control.tsx @@ -1,18 +1,18 @@ import * as React from 'react' import { - TLDraw, + Tldraw, ColorStyle, DashStyle, SizeStyle, - TLDrawDocument, - TLDrawShapeType, - TLDrawState, + TDDocument, + TDShapeType, + TldrawApp, } from '@tldraw/tldraw' export default function Controlled() { - const rDocument = React.useRef({ + const rDocument = React.useRef({ name: 'New Document', - version: TLDrawState.version, + version: TldrawApp.version, id: 'doc', pages: { page1: { @@ -20,7 +20,7 @@ export default function Controlled() { shapes: { rect1: { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, parentId: 'page1', name: 'Rectangle', childIndex: 1, @@ -34,7 +34,7 @@ export default function Controlled() { }, rect2: { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, parentId: 'page1', name: 'Rectangle', childIndex: 1, @@ -62,7 +62,7 @@ export default function Controlled() { }, }) - const [doc, setDoc] = React.useState(rDocument.current) + const [doc, setDoc] = React.useState(rDocument.current) React.useEffect(() => { let i = 0 @@ -105,5 +105,5 @@ export default function Controlled() { rDocument.current = state.document }, []) - return + return } diff --git a/example/src/readonly.tsx b/example/src/readonly.tsx index e1df58c97..f9ea4cc24 100644 --- a/example/src/readonly.tsx +++ b/example/src/readonly.tsx @@ -1,8 +1,8 @@ -import { TLDraw, TLDrawFile } from '@tldraw/tldraw' +import { Tldraw, TDFile } from '@tldraw/tldraw' import * as React from 'react' export default function ReadOnly(): JSX.Element { - const [file, setFile] = React.useState() + const [file, setFile] = React.useState() React.useEffect(() => { async function loadFile(): Promise { @@ -15,7 +15,7 @@ export default function ReadOnly(): JSX.Element { return (
    - +
    ) } diff --git a/example/src/ui-options.tsx b/example/src/ui-options.tsx index 2027c08c4..242415527 100644 --- a/example/src/ui-options.tsx +++ b/example/src/ui-options.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { TLDraw } from '@tldraw/tldraw' +import { Tldraw } from '@tldraw/tldraw' export default function UIOptions(): JSX.Element { return (
    - ` component as well as the data model that the component accepts. +This file contains the documentatin for the `` component as well as the data model that the component accepts. In addition to the docs written below, this project also includes **generated documentation**. To view the generated docs: @@ -10,38 +10,39 @@ In addition to the docs written below, this project also includes **generated do 2. Open the file at: ``` -/packages/tldraw/docs/classes/TLDrawState.html +/packages/tldraw/docs/classes/TldrawApp.html ``` -## `TLDraw` +## `tldraw` -The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API. **All props are optional.** +The `Tldraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TldrawApp`'s imperative API. **All props are optional.** -| Prop | Type | Description | -| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------- | -| `id` | `string` | An id under which to persist the component's state. | -| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. | -| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. | -| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. | -| `showMenu` | `boolean` | Whether to show the menu. | -| `showPages` | `boolean` | Whether to show the pages menu. | -| `showStyles` | `boolean` | Whether to show the styles menu. | -| `showTools` | `boolean` | Whether to show the tools. | -| `showUI` | `boolean` | Whether to show any UI other than the canvas. | -| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TLDrawState`. | -| `onPatch` | `Function` | Called when the state is updated via a patch. | -| `onCommand` | `Function` | Called when the state is updated via a command. | -| `onPersist` | `Function` | Called when the state is persisted after an action. | -| `onChange` | `Function` | Called when the `TLDrawState` updates for any reason. | -| `onUserChange` | `Function` | Called when the user's "presence" information changes. | -| `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. | -| `onRedo` | `Function` | Called when the `TLDrawState` updates after a redo. | -| `onSignIn` | `Function` | Called when the user selects Sign In from the menu. | -| `onSignOut` | `Function` | Called when the user selects Sign Out from the menu. | -| `onNewProject` | `Function` | Called when the user when the user creates a new project through the menu or through a keyboard shortcut. | -| `onSaveProject` | `Function` | Called when the user saves a project through the menu or through a keyboard shortcut. | -| `onSaveProjectAs` | `Function` | Called when the user saves a project as a new project through the menu or through a keyboard shortcut. | -| `onOpenProject` | `Function` | Called when the user opens new project through the menu or through a keyboard shortcut. | +| Prop | Type | Description | +| ----------------- | ------------ | --------------------------------------------------------------------------------------------------------- | +| `id` | `string` | An id under which to persist the component's state. | +| `document` | `TDDocument` | An initial [`TDDocument`](#TDDocument) object. | +| `currentPageId` | `string` | A current page id, referencing the `TDDocument` object provided via the `document` prop. | +| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. | +| `showMenu` | `boolean` | Whether to show the menu. | +| `showPages` | `boolean` | Whether to show the pages menu. | +| `showStyles` | `boolean` | Whether to show the styles menu. | +| `showTools` | `boolean` | Whether to show the tools. | +| `showUI` | `boolean` | Whether to show any UI other than the canvas. | +| `showSponsorLink` | `boolean` | Whether to show a sponsor link. | +| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TldrawApp`. | +| `onPatch` | `Function` | Called when the state is updated via a patch. | +| `onCommand` | `Function` | Called when the state is updated via a command. | +| `onPersist` | `Function` | Called when the state is persisted after an action. | +| `onChange` | `Function` | Called when the `TldrawApp` updates for any reason. | +| `onUserChange` | `Function` | Called when the user's "presence" information changes. | +| `onUndo` | `Function` | Called when the `TldrawApp` updates after an undo. | +| `onRedo` | `Function` | Called when the `TldrawApp` updates after a redo. | +| `onSignIn` | `Function` | Called when the user selects Sign In from the menu. | +| `onSignOut` | `Function` | Called when the user selects Sign Out from the menu. | +| `onNewProject` | `Function` | Called when the user when the user creates a new project through the menu or through a keyboard shortcut. | +| `onSaveProject` | `Function` | Called when the user saves a project through the menu or through a keyboard shortcut. | +| `onSaveProjectAs` | `Function` | Called when the user saves a project as a new project through the menu or through a keyboard shortcut. | +| `onOpenProject` | `Function` | Called when the user opens new project through the menu or through a keyboard shortcut. | > **Note**: For help with the file-related callbacks, see `useFileSystem`. @@ -50,30 +51,30 @@ The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported You can use the `useFileSystem` hook to get prepared callbacks for `onNewProject`, `onOpenProject`, `onSaveProject`, and `onSaveProjectAs`. These callbacks allow a user to save files via the [FileSystem](https://developer.mozilla.org/en-US/docs/Web/API/FileSystem) API. ```ts -import { TLDraw, useFileSystem } from '@tldraw/tldraw' +import { Tldraw, useFileSystem } from '@tldraw/tldraw' function App() { const fileSystemEvents = useFileSystem() - return + return } ``` -## `TLDrawDocument` +## `TDDocument` -You can initialize or control the `` component via its `document` property. A `TLDrawDocument` is an object with three properties: +You can initialize or control the `` component via its `document` property. A `TDDocument` is an object with three properties: - `id` - A unique ID for this document -- `pages` - A table of `TLDrawPage` objects +- `pages` - A table of `TDPage` objects - `pageStates` - A table of `TLPageState` objects - `version` - The document's version, used internally for migrations. ```ts -import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw' +import { TDDocument, TldrawApp } from '@tldraw/tldraw' -const myDocument: TLDrawDocument = { +const myDocument: TDDocument = { id: 'doc', - version: TLDrawState.version, + version: TldrawApp.version, pages: { page1: { id: 'page1', @@ -95,34 +96,34 @@ const myDocument: TLDrawDocument = { } function App() { - return + return } ``` -**Tip:** TLDraw is built on [@tldraw/core](https://github.com/tldraw/core). The pages and pageStates in TLDraw are objects containing `TLPage` and `TLPageState` objects from the core library. For more about these types, check out the [@tldraw/core](https://github.com/tldraw/core) documentation. +**Tip:** The pages and pageStates in tldraw are objects containing `TLPage` and `TLPageState` objects from the [@tldraw/core](https://github.com/tldraw/core) library. **Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page. ## Shapes -Your `TLPage` objects may include shapes: objects that fit one of the `TLDrawShape` interfaces listed below. All `TLDrawShapes` extends a common interface: +Your `TLPage` objects may include shapes: objects that fit one of the `TldrawShape` interfaces listed below. All `TldrawShapes` extends a common interface: -| Property | Type | Description | -| --------------------- | ---------------- | --------------------------------------------------------------- | -| `id` | `string` | A unique ID for the shape. | -| `name` | `string` | The shape's name. | -| `type` | `string` | The shape's type. | -| `parentId` | `string` | The ID of the shape's parent (a shape or its page). | -| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. | -| `point` | `number[]` | The `[x, y]` position of the shape. | -| `rotation` | `number[]` | (optional) The shape's rotation in radians. | -| `children` | `string[]` | (optional) The shape's child shape ids. | -| `handles` | `TLDrawHandle{}` | (optional) A table of `TLHandle` objects. | -| `isLocked` | `boolean` | (optional) True if the shape is locked. | -| `isHidden` | `boolean` | (optional) True if the shape is hidden. | -| `isEditing` | `boolean` | (optional) True if the shape is currently editing. | -| `isGenerated` | `boolean` | (optional) True if the shape is generated. | -| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. | +| Property | Type | Description | +| --------------------- | ------------ | --------------------------------------------------------------- | +| `id` | `string` | A unique ID for the shape. | +| `name` | `string` | The shape's name. | +| `type` | `string` | The shape's type. | +| `parentId` | `string` | The ID of the shape's parent (a shape or its page). | +| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. | +| `point` | `number[]` | The `[x, y]` position of the shape. | +| `rotation` | `number[]` | (optional) The shape's rotation in radians. | +| `children` | `string[]` | (optional) The shape's child shape ids. | +| `handles` | `TDHandle{}` | (optional) A table of `TLHandle` objects. | +| `isLocked` | `boolean` | (optional) True if the shape is locked. | +| `isHidden` | `boolean` | (optional) True if the shape is hidden. | +| `isEditing` | `boolean` | (optional) True if the shape is currently editing. | +| `isGenerated` | `boolean` | (optional) True if the shape is generated. | +| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. | > **Important:** In order for re-ordering to work, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1. @@ -197,26 +198,26 @@ A binding is a connection **from** one shape and **to** another shape. At the mo | `distance` | `number` | The distance from the bound point. | | `point` | `number[]` | A normalized point representing the bound point. | -## `TLDrawState` API +## `TldrawApp` API -You can change the `TLDraw` component's state through an imperative API called `TLDrawState`. To access this API, use the `onMount` callback, or any of the component's callback props, like `onPersist`. +You can change the `tldraw` component's state through an imperative API called `TldrawApp`. To access this API, use the `onMount` callback, or any of the component's callback props, like `onPersist`. ```tsx -import { TLDraw, TLDrawState } from '@tldraw/tldraw' +import { Tldraw, TldrawApp } from '@tldraw/tldraw' function App() { - const handleMount = React.useCallback((state: TLDrawState) => { + const handleMount = React.useCallback((state: TldrawApp) => { state.selectAll() }, []) - return + return } ``` -To view the full documentation of the `TLDrawState` API, generate the project's documentation by running `yarn docs` from the root folder, then open the file at: +To view the full documentation of the `TldrawApp` API, generate the project's documentation by running `yarn docs` from the root folder, then open the file at: ``` -/packages/tldraw/docs/classes/TLDrawState.html +/packages/tldraw/docs/classes/TldrawApp.html ``` Here are some useful methods: diff --git a/package.json b/package.json index 1cc6a8e48..5eefb01a6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "@steveruizok", "repository": { "type": "git", - "url": "git+https://github.com/tldraw/tldraw.git" + "url": "https://github.com/tldraw/tldraw.git" }, "license": "MIT", "workspaces": [ diff --git a/packages/tldraw/CHANGELOG.md b/packages/tldraw/CHANGELOG.md index a1d16085b..079836baa 100644 --- a/packages/tldraw/CHANGELOG.md +++ b/packages/tldraw/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.0.0 + +Breaking + +- Renames many props, components (e.g. `TLDrawState` to `TLDrawApp`) + +Improvements + +- Updates UI in toolbars, menus. +- Simplifies state and context. +- Adds and updtes tests. +- Renames TLDraw to tldraw throughout the app and documentation. +- Renames TLDrawState to TldrawApp, state to app. +- Improves action menu +- Improves dark colors +- Consolidates style menu +- Fixes performance bug with menus +- Fixes text formatting in Text shapes + +New + +- Adds shape menu +- Adds eraser tool (click or drag to erase) +- Adds `darkMode` prop for controlling dark mode UI. +- Double-click a tool icon to toggle "tool lock". This will prevent the app from returning to the select tool after creating a shape. + ## 0.1.17 - Fixes "shifting" bug with drawing tool. Finally! @@ -61,7 +87,7 @@ - Extracted into its own repository (`tldraw/core`) and open sourced! :clap: -### TLDraw +### tldraw - Updated with latest `@tldraw/core`, updated ShapeUtils API. @@ -71,31 +97,31 @@ - Major change to ShapeUtils API. -### TLDraw +### tldraw - Rewrite utils with new API. ## 0.0.126 -### TLDraw +### tldraw - Swap behavior of command and alt keys in arrow shapes. ## 0.0.125 -### TLDraw +### tldraw - Bug fixes. ## 0.0.124 -### TLDraw +### tldraw - Fix typings. ## 0.0.123 -### TLDraw +### tldraw - Adds bound shape controls. - Drag a bound shape control to translate shapes in that direction. @@ -104,7 +130,7 @@ ## 0.0.122 -### TLDraw +### tldraw - Adds snapping for transforming shapes. @@ -114,20 +140,20 @@ - Adds `snapLines`. -### TLDraw +### tldraw - Adds shape snapping while translating. Hold Command/Control to disable while dragging. ## 0.0.120 -### TLDraw +### tldraw - Improves rectangle rendering. - Improves zoom to fit and zoom to selection. ## 0.0.119 -### TLDraw +### tldraw - Fixes bug with bound arrows after undo. @@ -137,7 +163,7 @@ - Improves multiplayer features. -### TLDraw +### tldraw - Fixes bugs in text, arrows, stickies. - Adds start binding for new arrows. @@ -151,7 +177,7 @@ - Improves rendering on Safari. -### TLDraw +### tldraw - Improves rendering on Safari. - Minor bug fixes around selection. @@ -164,32 +190,32 @@ - Adds [side cloning](https://github.com/tldraw/tldraw/pull/149). - Improves rendering. -### TLDraw +### tldraw - Adds sticky note [side cloning](https://github.com/tldraw/tldraw/pull/149). ## 0.0.114 -### TLDraw +### tldraw - Improves fills for filled shapes. ## 0.0.113 -### TLDraw +### tldraw - Improves grouping and ungrouping. ## 0.0.112 -### TLDraw +### tldraw -- Fixes centering on embedded TLDraw components. +- Fixes centering on embedded tldraw components. - Removes expensive calls to window. ## 0.0.111 -### TLDraw +### tldraw - Adjust stroke widths and sizes. - Fixes a bug on very small dashed shapes. @@ -200,7 +226,7 @@ - Adds `user` and `users` props (optional) for multiplayer cursors. This feature is very lightly implemented. -### TLDraw +### tldraw - Adds multiplayer support. - Adds `showMenu` and `showPages` props. @@ -208,7 +234,7 @@ ## 0.0.109 -### TLDraw +### tldraw - Bumps perfect-freehand - Fixes dots for small sized draw shapes @@ -217,6 +243,6 @@ - Adds CHANGELOG. Only 108 releases late! -### TLDraw +### tldraw - Fixes a bug with bounding boxes on arrows. diff --git a/packages/tldraw/README.md b/packages/tldraw/README.md index fdf9f8e3b..9c78304f2 100644 --- a/packages/tldraw/README.md +++ b/packages/tldraw/README.md @@ -4,10 +4,10 @@ # @tldraw/tldraw -This package contains the [TLDraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. +This package contains the [tldraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. -🎨 Want to build your own TLDraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). +🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). 💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). -For documentation, see the [TLDraw](https://github.com/tldraw) repository. +For documentation, see the [tldraw](https://github.com/tldraw) repository. diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index f97412030..329e80db0 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -5,7 +5,7 @@ "author": "@steveruizok", "repository": { "type": "git", - "url": "git+https://github.com/tldraw/tldraw.git" + "url": "https://github.com/tldraw/tldraw.git" }, "license": "MIT", "keywords": [ @@ -59,4 +59,4 @@ "tsconfig-replace-paths": "^0.0.5" }, "gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a" -} +} \ No newline at end of file diff --git a/packages/tldraw/src/TLDraw.test.tsx b/packages/tldraw/src/Tldraw.spec.tsx similarity index 69% rename from packages/tldraw/src/TLDraw.test.tsx rename to packages/tldraw/src/Tldraw.spec.tsx index ed2127f77..1c4ea5390 100644 --- a/packages/tldraw/src/TLDraw.test.tsx +++ b/packages/tldraw/src/Tldraw.spec.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import { render, waitFor } from '@testing-library/react' -import { TLDraw } from './TLDraw' +import { Tldraw } from './Tldraw' -describe('tldraw', () => { +describe('Tldraw', () => { test('mounts component and calls onMount', async () => { const onMount = jest.fn() - render() + render() await waitFor(onMount) }) test('mounts component and calls onMount when id is present', async () => { const onMount = jest.fn() - render() + render() await waitFor(onMount) }) }) diff --git a/packages/tldraw/src/TLDraw.tsx b/packages/tldraw/src/Tldraw.tsx similarity index 58% rename from packages/tldraw/src/TLDraw.tsx rename to packages/tldraw/src/Tldraw.tsx index 0daabb97f..69ea3c696 100644 --- a/packages/tldraw/src/TLDraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -2,15 +2,9 @@ import * as React from 'react' import { IdProvider } from '@radix-ui/react-id' import { Renderer } from '@tldraw/core' import { styled, dark } from '~styles' -import { TLDrawSnapshot, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types' -import { TLDrawCallbacks, TLDrawState } from '~state' -import { - TLDrawContext, - TLDrawContextType, - useStylesheet, - useKeyboardShortcuts, - useTLDrawContext, -} from '~hooks' +import { TDDocument, TDStatus, TDUser } from '~types' +import { TldrawApp, TDCallbacks } from '~state' +import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from '~hooks' import { shapeUtils } from '~state/shapes' import { ToolsPanel } from '~components/ToolsPanel' import { TopPanel } from '~components/TopPanel' @@ -18,29 +12,7 @@ import { TLDR } from '~state/TLDR' import { ContextMenu } from '~components/ContextMenu' import { FocusButton } from '~components/FocusButton/FocusButton' -// Selectors -const isInSelectSelector = (s: TLDrawSnapshot) => s.appState.activeTool === 'select' - -const isHideBoundsShapeSelector = (s: TLDrawSnapshot) => { - const { shapes } = s.document.pages[s.appState.currentPageId] - const { selectedIds } = s.document.pageStates[s.appState.currentPageId] - return ( - selectedIds.length === 1 && - selectedIds.every((id) => TLDR.getShapeUtils(shapes[id].type).hideBounds) - ) -} - -const pageSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId] - -const snapLinesSelector = (s: TLDrawSnapshot) => s.appState.snapLines - -const usersSelector = (s: TLDrawSnapshot) => s.room?.users - -const pageStateSelector = (s: TLDrawSnapshot) => s.document.pageStates[s.appState.currentPageId] - -const settingsSelector = (s: TLDrawSnapshot) => s.settings - -export interface TLDrawProps extends TLDrawCallbacks { +export interface TldrawProps extends TDCallbacks { /** * (optional) If provided, the component will load / persist state under this key. */ @@ -49,7 +21,7 @@ export interface TLDrawProps extends TLDrawCallbacks { /** * (optional) The document to load or update from. */ - document?: TLDrawDocument + document?: TDDocument /** * (optional) The current page id. @@ -86,6 +58,11 @@ export interface TLDrawProps extends TLDrawCallbacks { */ showTools?: boolean + /** + * (optional) Whether to show a sponsor link for Tldraw. + */ + showSponsorLink?: boolean + /** * (optional) Whether to show the UI. */ @@ -96,69 +73,81 @@ export interface TLDrawProps extends TLDrawCallbacks { */ readOnly?: boolean + /** + * (optional) Whether to to show the app's dark mode UI. + */ + darkMode?: boolean + /** * (optional) A callback to run when the component mounts. */ - onMount?: (state: TLDrawState) => void + onMount?: (state: TldrawApp) => void + /** * (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut. */ - onNewProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void + /** * (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut. */ - onSaveProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void + /** * (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut. */ - onSaveProjectAs?: (state: TLDrawState, e?: KeyboardEvent) => void + onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void + /** * (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut. */ - onOpenProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void + /** * (optional) A callback to run when the user signs in via the menu. */ - onSignIn?: (state: TLDrawState) => void + onSignIn?: (state: TldrawApp) => void + /** * (optional) A callback to run when the user signs out via the menu. */ - onSignOut?: (state: TLDrawState) => void + onSignOut?: (state: TldrawApp) => void /** * (optional) A callback to run when the user creates a new project. */ - onUserChange?: (state: TLDrawState, user: TLDrawUser) => void + onUserChange?: (state: TldrawApp, user: TDUser) => void /** * (optional) A callback to run when the component's state changes. */ - onChange?: (state: TLDrawState, reason?: string) => void + onChange?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the state is patched. */ - onPatch?: (state: TLDrawState, reason?: string) => void + onPatch?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the state is changed with a command. */ - onCommand?: (state: TLDrawState, reason?: string) => void + onCommand?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the state is persisted. */ - onPersist?: (state: TLDrawState) => void + onPersist?: (state: TldrawApp) => void /** * (optional) A callback to run when the user undos. */ - onUndo?: (state: TLDrawState) => void + onUndo?: (state: TldrawApp) => void /** * (optional) A callback to run when the user redos. */ - onRedo?: (state: TLDrawState) => void + onRedo?: (state: TldrawApp) => void } -export function TLDraw({ +export function Tldraw({ id, document, currentPageId, + darkMode = false, autofocus = true, showMenu = true, showPages = true, @@ -167,6 +156,7 @@ export function TLDraw({ showStyles = true, showUI = true, readOnly = false, + showSponsorLink = false, onMount, onChange, onUserChange, @@ -181,12 +171,13 @@ export function TLDraw({ onPersist, onPatch, onCommand, -}: TLDrawProps) { +}: TldrawProps) { const [sId, setSId] = React.useState(id) - const [state, setState] = React.useState( + // Create a new app when the component mounts. + const [app, setApp] = React.useState( () => - new TLDrawState(id, { + new TldrawApp(id, { onMount, onChange, onUserChange, @@ -204,15 +195,11 @@ export function TLDraw({ }) ) - const [context, setContext] = React.useState(() => ({ - state, - useSelector: state.useStore, - })) - + // Create a new app if the `id` prop changes. React.useEffect(() => { if (id === sId) return - const newState = new TLDrawState(id, { + const newApp = new TldrawApp(id, { onMount, onChange, onUserChange, @@ -231,31 +218,42 @@ export function TLDraw({ setSId(id) - setContext((ctx) => ({ - ...ctx, - state: newState, - useSelector: newState.useStore, - })) - - setState(newState) + setApp(newApp) }, [sId, id]) - React.useEffect(() => { - state.readOnly = readOnly - }, [state, readOnly]) - + // Update the document if the `document` prop changes but the ids, + // are the same, or else load a new document if the ids are different. React.useEffect(() => { if (!document) return - if (document.id === state.document.id) { - state.updateDocument(document) + if (document.id === app.document.id) { + app.updateDocument(document) } else { - state.loadDocument(document) + app.loadDocument(document) } - }, [document, state]) + }, [document, app]) + // Change the page when the `currentPageId` prop changes React.useEffect(() => { - state.callbacks = { + if (!currentPageId) return + app.changePage(currentPageId) + }, [currentPageId, app]) + + // Toggle the app's readOnly mode when the `readOnly` prop changes + React.useEffect(() => { + app.readOnly = readOnly + }, [app, readOnly]) + + // Toggle the app's readOnly mode when the `readOnly` prop changes + React.useEffect(() => { + if (darkMode && !app.settings.isDarkMode) { + // app.toggleDarkMode() + } + }, [app, darkMode]) + + // Update the app's callbacks when any callback changes. + React.useEffect(() => { + app.callbacks = { onMount, onChange, onUserChange, @@ -272,7 +270,7 @@ export function TLDraw({ onPersist, } }, [ - state, + app, onMount, onChange, onUserChange, @@ -291,12 +289,11 @@ export function TLDraw({ // Use the `key` to ensure that new selector hooks are made when the id changes return ( - + - - + ) } -interface InnerTLDrawProps { +interface InnerTldrawProps { id?: string - currentPageId?: string autofocus: boolean showPages: boolean showMenu: boolean @@ -321,44 +318,49 @@ interface InnerTLDrawProps { showStyles: boolean showUI: boolean showTools: boolean + showSponsorLink: boolean readOnly: boolean } -const InnerTLDraw = React.memo(function InnerTLDraw({ +const InnerTldraw = React.memo(function InnerTldraw({ id, - currentPageId, autofocus, showPages, showMenu, showZoom, showStyles, showTools, + showSponsorLink, readOnly, showUI, -}: InnerTLDrawProps) { - const { state, useSelector } = useTLDrawContext() +}: InnerTldrawProps) { + const app = useTldrawApp() const rWrapper = React.useRef(null) - const page = useSelector(pageSelector) + const state = app.useStore() - const pageState = useSelector(pageStateSelector) + const { document, settings, appState, room } = state - const snapLines = useSelector(snapLinesSelector) + const isSelecting = state.appState.activeTool === 'select' - const users = useSelector(usersSelector) + const page = document.pages[appState.currentPageId] + const pageState = document.pageStates[page.id] + const { selectedIds } = state.document.pageStates[page.id] - const settings = useSelector(settingsSelector) + const isHideBoundsShape = + pageState.selectedIds.length === 1 && + TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideBounds - const isSelecting = useSelector(isInSelectSelector) + const isHideResizeHandlesShape = + selectedIds.length === 1 && + TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideResizeHandles - const isHideBoundsShape = useSelector(isHideBoundsShapeSelector) - - const isInSession = state.session !== undefined + const isInSession = app.session !== undefined // Hide bounds when not using the select tool, or when the only selected shape has handles const hideBounds = - (isInSession && state.session?.constructor.name !== 'BrushSession') || + (isInSession && app.session?.constructor.name !== 'BrushSession') || !isSelecting || isHideBoundsShape || !!pageState.editingId @@ -368,10 +370,12 @@ const InnerTLDraw = React.memo(function InnerTLDraw({ // Hide indicators when not using the select tool, or when in session const hideIndicators = - (isInSession && state.appState.status !== TLDrawStatus.Brushing) || !isSelecting + (isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting // Custom rendering meta, with dark mode for shapes - const meta = React.useMemo(() => ({ isDarkMode: settings.isDarkMode }), [settings.isDarkMode]) + const meta = React.useMemo(() => { + return { isDarkMode: settings.isDarkMode } + }, [settings.isDarkMode]) // Custom theme, based on darkmode const theme = React.useMemo(() => { @@ -381,7 +385,7 @@ const InnerTLDraw = React.memo(function InnerTLDraw({ brushStroke: 'rgba(180, 180, 180, .25)', selected: 'rgba(38, 150, 255, 1.000)', selectFill: 'rgba(38, 150, 255, 0.05)', - background: '#343d45', + background: '#212529', foreground: '#49555f', } } @@ -389,11 +393,6 @@ const InnerTLDraw = React.memo(function InnerTLDraw({ return {} }, [settings.isDarkMode]) - React.useEffect(() => { - if (!currentPageId) return - state.changePage(currentPageId) - }, [currentPageId, state]) - // When the context menu is blurred, close the menu by sending pointer events // to the context menu's ref. This is a hack around the fact that certain shapes // stop event propagation, which causes the menu to stay open even when blurred. @@ -415,72 +414,73 @@ const InnerTLDraw = React.memo(function InnerTLDraw({ shapeUtils={shapeUtils} page={page} pageState={pageState} - snapLines={snapLines} - users={users} - userId={state.state.room?.userId} + snapLines={appState.snapLines} + users={room?.users} + userId={room?.userId} theme={theme} meta={meta} hideBounds={hideBounds} hideHandles={hideHandles} + hideResizeHandles={isHideResizeHandlesShape} hideIndicators={hideIndicators} hideBindingHandles={!settings.showBindingHandles} hideCloneHandles={!settings.showCloneHandles} hideRotateHandles={!settings.showRotateHandles} - onPinchStart={state.onPinchStart} - onPinchEnd={state.onPinchEnd} - onPinch={state.onPinch} - onPan={state.onPan} - onZoom={state.onZoom} - onPointerDown={state.onPointerDown} - onPointerMove={state.onPointerMove} - onPointerUp={state.onPointerUp} - onPointCanvas={state.onPointCanvas} - onDoubleClickCanvas={state.onDoubleClickCanvas} - onRightPointCanvas={state.onRightPointCanvas} - onDragCanvas={state.onDragCanvas} - onReleaseCanvas={state.onReleaseCanvas} - onPointShape={state.onPointShape} - onDoubleClickShape={state.onDoubleClickShape} - onRightPointShape={state.onRightPointShape} - onDragShape={state.onDragShape} - onHoverShape={state.onHoverShape} - onUnhoverShape={state.onUnhoverShape} - onReleaseShape={state.onReleaseShape} - onPointBounds={state.onPointBounds} - onDoubleClickBounds={state.onDoubleClickBounds} - onRightPointBounds={state.onRightPointBounds} - onDragBounds={state.onDragBounds} - onHoverBounds={state.onHoverBounds} - onUnhoverBounds={state.onUnhoverBounds} - onReleaseBounds={state.onReleaseBounds} - onPointBoundsHandle={state.onPointBoundsHandle} - onDoubleClickBoundsHandle={state.onDoubleClickBoundsHandle} - onRightPointBoundsHandle={state.onRightPointBoundsHandle} - onDragBoundsHandle={state.onDragBoundsHandle} - onHoverBoundsHandle={state.onHoverBoundsHandle} - onUnhoverBoundsHandle={state.onUnhoverBoundsHandle} - onReleaseBoundsHandle={state.onReleaseBoundsHandle} - onPointHandle={state.onPointHandle} - onDoubleClickHandle={state.onDoubleClickHandle} - onRightPointHandle={state.onRightPointHandle} - onDragHandle={state.onDragHandle} - onHoverHandle={state.onHoverHandle} - onUnhoverHandle={state.onUnhoverHandle} - onReleaseHandle={state.onReleaseHandle} - onError={state.onError} - onRenderCountChange={state.onRenderCountChange} - onShapeChange={state.onShapeChange} - onShapeBlur={state.onShapeBlur} - onShapeClone={state.onShapeClone} - onBoundsChange={state.updateBounds} - onKeyDown={state.onKeyDown} - onKeyUp={state.onKeyUp} + onPinchStart={app.onPinchStart} + onPinchEnd={app.onPinchEnd} + onPinch={app.onPinch} + onPan={app.onPan} + onZoom={app.onZoom} + onPointerDown={app.onPointerDown} + onPointerMove={app.onPointerMove} + onPointerUp={app.onPointerUp} + onPointCanvas={app.onPointCanvas} + onDoubleClickCanvas={app.onDoubleClickCanvas} + onRightPointCanvas={app.onRightPointCanvas} + onDragCanvas={app.onDragCanvas} + onReleaseCanvas={app.onReleaseCanvas} + onPointShape={app.onPointShape} + onDoubleClickShape={app.onDoubleClickShape} + onRightPointShape={app.onRightPointShape} + onDragShape={app.onDragShape} + onHoverShape={app.onHoverShape} + onUnhoverShape={app.onUnhoverShape} + onReleaseShape={app.onReleaseShape} + onPointBounds={app.onPointBounds} + onDoubleClickBounds={app.onDoubleClickBounds} + onRightPointBounds={app.onRightPointBounds} + onDragBounds={app.onDragBounds} + onHoverBounds={app.onHoverBounds} + onUnhoverBounds={app.onUnhoverBounds} + onReleaseBounds={app.onReleaseBounds} + onPointBoundsHandle={app.onPointBoundsHandle} + onDoubleClickBoundsHandle={app.onDoubleClickBoundsHandle} + onRightPointBoundsHandle={app.onRightPointBoundsHandle} + onDragBoundsHandle={app.onDragBoundsHandle} + onHoverBoundsHandle={app.onHoverBoundsHandle} + onUnhoverBoundsHandle={app.onUnhoverBoundsHandle} + onReleaseBoundsHandle={app.onReleaseBoundsHandle} + onPointHandle={app.onPointHandle} + onDoubleClickHandle={app.onDoubleClickHandle} + onRightPointHandle={app.onRightPointHandle} + onDragHandle={app.onDragHandle} + onHoverHandle={app.onHoverHandle} + onUnhoverHandle={app.onUnhoverHandle} + onReleaseHandle={app.onReleaseHandle} + onError={app.onError} + onRenderCountChange={app.onRenderCountChange} + onShapeChange={app.onShapeChange} + onShapeBlur={app.onShapeBlur} + onShapeClone={app.onShapeClone} + onBoundsChange={app.updateBounds} + onKeyDown={app.onKeyDown} + onKeyUp={app.onKeyUp} /> {showUI && ( {settings.isFocusMode ? ( - + ) : ( <> {showTools && !readOnly && } @@ -539,6 +540,13 @@ const StyledLayout = styled('div', { width: '100%', zIndex: 1, }, + + '& input, textarea, button, select, label, button': { + webkitTouchCallout: 'none', + webkitUserSelect: 'none', + '-webkit-tap-highlight-color': 'transparent', + 'tap-highlight-color': 'transparent', + }, }) const StyledUI = styled('div', { diff --git a/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx b/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx index ab0405ed0..7cd6e4f16 100644 --- a/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx +++ b/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import { ContextMenuItem } from '@radix-ui/react-context-menu' import { RowButton, RowButtonProps } from '~components/RowButton' -export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => { +export const CMRowButton = ({ ...rest }: RowButtonProps) => { return ( - + ) diff --git a/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx index e4e7b1fc7..7fac35f22 100644 --- a/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { styled } from '~styles' import * as RadixContextMenu from '@radix-ui/react-context-menu' -import { useTLDrawContext } from '~hooks' -import { TLDrawSnapshot, AlignType, DistributeType, StretchType } from '~types' +import { useTldrawApp } from '~hooks' +import { TDSnapshot, AlignType, DistributeType, StretchType } from '~types' import { AlignBottomIcon, AlignCenterHorizontallyIcon, @@ -21,21 +21,21 @@ import { CMTriggerButton } from './CMTriggerButton' import { Divider } from '~components/Divider' import { MenuContent } from '~components/MenuContent' -const has1SelectedIdsSelector = (s: TLDrawSnapshot) => { +const has1SelectedIdsSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0 } -const has2SelectedIdsSelector = (s: TLDrawSnapshot) => { +const has2SelectedIdsSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1 } -const has3SelectedIdsSelector = (s: TLDrawSnapshot) => { +const has3SelectedIdsSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2 } -const isDebugModeSelector = (s: TLDrawSnapshot) => { +const isDebugModeSelector = (s: TDSnapshot) => { return s.settings.isDebugMode } -const hasGroupSelectedSelector = (s: TLDrawSnapshot) => { +const hasGroupSelectedSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.some( (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined ) @@ -49,77 +49,81 @@ interface ContextMenuProps { } export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element => { - const { state, useSelector } = useTLDrawContext() - const hasSelection = useSelector(has1SelectedIdsSelector) - const hasTwoOrMore = useSelector(has2SelectedIdsSelector) - const hasThreeOrMore = useSelector(has3SelectedIdsSelector) - const isDebugMode = useSelector(isDebugModeSelector) - const hasGroupSelected = useSelector(hasGroupSelectedSelector) + const app = useTldrawApp() + const hasSelection = app.useStore(has1SelectedIdsSelector) + const hasTwoOrMore = app.useStore(has2SelectedIdsSelector) + const hasThreeOrMore = app.useStore(has3SelectedIdsSelector) + const isDebugMode = app.useStore(isDebugModeSelector) + const hasGroupSelected = app.useStore(hasGroupSelectedSelector) const rContent = React.useRef(null) const handleFlipHorizontal = React.useCallback(() => { - state.flipHorizontal() - }, [state]) + app.flipHorizontal() + }, [app]) const handleFlipVertical = React.useCallback(() => { - state.flipVertical() - }, [state]) + app.flipVertical() + }, [app]) const handleDuplicate = React.useCallback(() => { - state.duplicate() - }, [state]) + app.duplicate() + }, [app]) + + const handleLock = React.useCallback(() => { + app.toggleLocked() + }, [app]) const handleGroup = React.useCallback(() => { - state.group() - }, [state]) + app.group() + }, [app]) const handleMoveToBack = React.useCallback(() => { - state.moveToBack() - }, [state]) + app.moveToBack() + }, [app]) const handleMoveBackward = React.useCallback(() => { - state.moveBackward() - }, [state]) + app.moveBackward() + }, [app]) const handleMoveForward = React.useCallback(() => { - state.moveForward() - }, [state]) + app.moveForward() + }, [app]) const handleMoveToFront = React.useCallback(() => { - state.moveToFront() - }, [state]) + app.moveToFront() + }, [app]) const handleDelete = React.useCallback(() => { - state.delete() - }, [state]) + app.delete() + }, [app]) const handleCopyJson = React.useCallback(() => { - state.copyJson() - }, [state]) + app.copyJson() + }, [app]) const handleCopy = React.useCallback(() => { - state.copy() - }, [state]) + app.copy() + }, [app]) const handlePaste = React.useCallback(() => { - state.paste() - }, [state]) + app.paste() + }, [app]) const handleCopySvg = React.useCallback(() => { - state.copySvg() - }, [state]) + app.copySvg() + }, [app]) const handleUndo = React.useCallback(() => { - state.undo() - }, [state]) + app.undo() + }, [app]) const handleRedo = React.useCallback(() => { - state.redo() - }, [state]) + app.redo() + }, [app]) return ( - + {children} {hasSelection ? ( <> - + + Duplicate + + Flip Horizontal - + Flip Vertical - - Duplicate + + Lock / Unlock {(hasTwoOrMore || hasGroupSelected) && } {hasTwoOrMore && ( - + Group )} {hasGroupSelected && ( - + Ungroup )} - + To Front - + Forward - + Backward - + To Back @@ -175,30 +182,30 @@ export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element /> )} - + Copy - + Copy as SVG - {isDebugMode && Copy as JSON} - + {isDebugMode && Copy as JSON} + Paste - + Delete ) : ( <> - + Paste - + Undo - + Redo @@ -215,84 +222,84 @@ function AlignDistributeSubMenu({ hasTwoOrMore: boolean hasThreeOrMore: boolean }) { - const { state } = useTLDrawContext() + const app = useTldrawApp() const alignTop = React.useCallback(() => { - state.align(AlignType.Top) - }, [state]) + app.align(AlignType.Top) + }, [app]) const alignCenterVertical = React.useCallback(() => { - state.align(AlignType.CenterVertical) - }, [state]) + app.align(AlignType.CenterVertical) + }, [app]) const alignBottom = React.useCallback(() => { - state.align(AlignType.Bottom) - }, [state]) + app.align(AlignType.Bottom) + }, [app]) const stretchVertically = React.useCallback(() => { - state.stretch(StretchType.Vertical) - }, [state]) + app.stretch(StretchType.Vertical) + }, [app]) const distributeVertically = React.useCallback(() => { - state.distribute(DistributeType.Vertical) - }, [state]) + app.distribute(DistributeType.Vertical) + }, [app]) const alignLeft = React.useCallback(() => { - state.align(AlignType.Left) - }, [state]) + app.align(AlignType.Left) + }, [app]) const alignCenterHorizontal = React.useCallback(() => { - state.align(AlignType.CenterHorizontal) - }, [state]) + app.align(AlignType.CenterHorizontal) + }, [app]) const alignRight = React.useCallback(() => { - state.align(AlignType.Right) - }, [state]) + app.align(AlignType.Right) + }, [app]) const stretchHorizontally = React.useCallback(() => { - state.stretch(StretchType.Horizontal) - }, [state]) + app.stretch(StretchType.Horizontal) + }, [app]) const distributeHorizontally = React.useCallback(() => { - state.distribute(DistributeType.Horizontal) - }, [state]) + app.distribute(DistributeType.Horizontal) + }, [app]) return ( - + Align / Distribute - + - + - + - + {hasThreeOrMore && ( - + )} - + - + - + - + {hasThreeOrMore && ( - + )} @@ -319,13 +326,13 @@ const StyledGridContent = styled(MenuContent, { /* ------------------ Move to Page ------------------ */ -const currentPageIdSelector = (s: TLDrawSnapshot) => s.appState.currentPageId -const documentPagesSelector = (s: TLDrawSnapshot) => s.document.pages +const currentPageIdSelector = (s: TDSnapshot) => s.appState.currentPageId +const documentPagesSelector = (s: TDSnapshot) => s.document.pages function MoveToPageMenu(): JSX.Element | null { - const { state, useSelector } = useTLDrawContext() - const currentPageId = useSelector(currentPageIdSelector) - const documentPages = useSelector(documentPagesSelector) + const app = useTldrawApp() + const currentPageId = app.useStore(currentPageIdSelector) + const documentPages = app.useStore(documentPagesSelector) const sorted = Object.values(documentPages) .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) @@ -342,7 +349,7 @@ function MoveToPageMenu(): JSX.Element | null { state.moveToPage(id)} + onClick={() => app.moveToPage(id)} > {name || `Page ${i}`} diff --git a/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx index f8dc4b50c..b30368dcc 100644 --- a/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx +++ b/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx @@ -1,18 +1,21 @@ import * as React from 'react' import { CheckboxItem } from '@radix-ui/react-dropdown-menu' -import { RowButton } from '~components/RowButton' +import { RowButton, RowButtonProps } from '~components/RowButton' +import { preventEvent } from '~components/preventEvent' interface DMCheckboxItemProps { checked: boolean disabled?: boolean onCheckedChange: (isChecked: boolean) => void children: React.ReactNode + variant?: RowButtonProps['variant'] kbd?: string } export function DMCheckboxItem({ checked, disabled = false, + variant, onCheckedChange, kbd, children, @@ -20,12 +23,13 @@ export function DMCheckboxItem({ return ( - + {children} diff --git a/packages/tldraw/src/components/DropdownMenu/DMContent.tsx b/packages/tldraw/src/components/DropdownMenu/DMContent.tsx index 4e093c69a..cc0536aab 100644 --- a/packages/tldraw/src/components/DropdownMenu/DMContent.tsx +++ b/packages/tldraw/src/components/DropdownMenu/DMContent.tsx @@ -2,18 +2,29 @@ import * as React from 'react' import { Content } from '@radix-ui/react-dropdown-menu' import { styled } from '~styles/stitches.config' import { MenuContent } from '~components/MenuContent' +import { stopPropagation } from '~components/stopPropagation' export interface DMContentProps { - variant?: 'grid' | 'menu' | 'horizontal' + variant?: 'menu' | 'horizontal' align?: 'start' | 'center' | 'end' + sideOffset?: number children: React.ReactNode } -const preventDefault = (e: Event) => e.stopPropagation() - -export function DMContent({ children, align, variant }: DMContentProps): JSX.Element { +export function DMContent({ + sideOffset = 8, + children, + align, + variant, +}: DMContentProps): JSX.Element { return ( - + {children} ) @@ -25,11 +36,6 @@ export const StyledContent = styled(MenuContent, { minWidth: 0, variants: { variant: { - grid: { - display: 'grid', - gridTemplateColumns: 'repeat(4, auto)', - gap: 0, - }, horizontal: { flexDirection: 'row', }, diff --git a/packages/tldraw/src/components/DropdownMenu/DMItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMItem.tsx index 8388a4c0e..f28fabfa1 100644 --- a/packages/tldraw/src/components/DropdownMenu/DMItem.tsx +++ b/packages/tldraw/src/components/DropdownMenu/DMItem.tsx @@ -2,7 +2,10 @@ import * as React from 'react' import { Item } from '@radix-ui/react-dropdown-menu' import { RowButton, RowButtonProps } from '~components/RowButton' -export function DMItem({ onSelect, ...rest }: RowButtonProps): JSX.Element { +export function DMItem({ + onSelect, + ...rest +}: RowButtonProps & { onSelect?: (event: Event) => void }): JSX.Element { return ( diff --git a/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx index 1ddf652a0..9d45e4fc5 100644 --- a/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx +++ b/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx @@ -16,11 +16,32 @@ export const DMRadioItem = styled(RadioItem, { pointerEvents: 'all', cursor: 'pointer', - '&:focus': { - backgroundColor: '$hover', + variants: { + isActive: { + true: { + backgroundColor: '$selected', + color: '$panel', + }, + false: {}, + }, + bp: { + mobile: {}, + small: {}, + }, }, - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - }, + compoundVariants: [ + { + isActive: false, + bp: 'small', + css: { + '&:focus': { + backgroundColor: '$hover', + }, + '&:hover:not(:disabled)': { + backgroundColor: '$hover', + }, + }, + }, + ], }) diff --git a/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx b/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx index ba0018784..a997e2731 100644 --- a/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx +++ b/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx @@ -1,15 +1,15 @@ import * as React from 'react' import { Trigger } from '@radix-ui/react-dropdown-menu' -import { ToolButton } from '~components/ToolButton' +import { ToolButton, ToolButtonProps } from '~components/ToolButton' -interface DMTriggerIconProps { +interface DMTriggerIconProps extends ToolButtonProps { children: React.ReactNode } -export function DMTriggerIcon({ children }: DMTriggerIconProps) { +export function DMTriggerIcon({ children, ...rest }: DMTriggerIconProps) { return ( - {children} + {children} ) } diff --git a/packages/tldraw/src/components/FocusButton/FocusButton.tsx b/packages/tldraw/src/components/FocusButton/FocusButton.tsx index 4c023ac99..265f30f36 100644 --- a/packages/tldraw/src/components/FocusButton/FocusButton.tsx +++ b/packages/tldraw/src/components/FocusButton/FocusButton.tsx @@ -23,7 +23,7 @@ const StyledButtonContainer = styled('div', { backgroundColor: 'transparent', '& svg': { - color: '$muted', + color: '$text', }, '&:hover svg': { diff --git a/packages/tldraw/src/components/Kbd/Kbd.tsx b/packages/tldraw/src/components/Kbd/Kbd.tsx index 8ddb8dd76..f53d4c03b 100644 --- a/packages/tldraw/src/components/Kbd/Kbd.tsx +++ b/packages/tldraw/src/components/Kbd/Kbd.tsx @@ -33,8 +33,9 @@ export const StyledKbd = styled('kbd', { textAlign: 'center', fontSize: '$0', fontFamily: '$ui', - fontWeight: 400, color: '$text', + background: 'none', + fontWeight: 400, gap: '$1', display: 'flex', alignItems: 'center', @@ -51,7 +52,7 @@ export const StyledKbd = styled('kbd', { variant: { tooltip: { '& > span': { - color: '$tooltipText', + color: '$tooltipContrast', background: '$overlayContrast', boxShadow: '$key', width: '20px', diff --git a/packages/tldraw/src/components/Panel/Panel.tsx b/packages/tldraw/src/components/Panel/Panel.tsx index 6ff8730ab..12eed0d7d 100644 --- a/packages/tldraw/src/components/Panel/Panel.tsx +++ b/packages/tldraw/src/components/Panel/Panel.tsx @@ -6,17 +6,28 @@ export const Panel = styled('div', { flexDirection: 'row', boxShadow: '$panel', padding: '$2', + border: '1px solid $panelContrast', + gap: 0, variants: { side: { center: { - borderTopLeftRadius: '$4', - borderTopRightRadius: '$4', + borderRadius: '$4', }, left: { + padding: 0, + borderTop: 0, + borderLeft: 0, + borderTopRightRadius: '$1', borderBottomRightRadius: '$3', + borderBottomLeftRadius: '$1', }, right: { + padding: 0, + borderTop: 0, + borderRight: 0, + borderTopLeftRadius: '$1', borderBottomLeftRadius: '$3', + borderBottomRightRadius: '$1', }, }, }, diff --git a/packages/tldraw/src/components/RowButton/RowButton.tsx b/packages/tldraw/src/components/RowButton/RowButton.tsx index 0df3328af..5db694f9b 100644 --- a/packages/tldraw/src/components/RowButton/RowButton.tsx +++ b/packages/tldraw/src/components/RowButton/RowButton.tsx @@ -7,10 +7,12 @@ import { SmallIcon } from '~components/SmallIcon' import { styled } from '~styles' export interface RowButtonProps { - onSelect?: () => void + onClick?: React.MouseEventHandler children: React.ReactNode disabled?: boolean kbd?: string + variant?: 'wide' + isSponsor?: boolean isActive?: boolean isWarning?: boolean hasIndicator?: boolean @@ -20,12 +22,14 @@ export interface RowButtonProps { export const RowButton = React.forwardRef( ( { - onSelect, + onClick, isActive = false, isWarning = false, hasIndicator = false, hasArrow = false, disabled = false, + isSponsor = false, + variant, kbd, children, ...rest @@ -38,8 +42,10 @@ export const RowButton = React.forwardRef( bp={breakpoints} isWarning={isWarning} isActive={isActive} + isSponsor={isSponsor} disabled={disabled} - onPointerDown={onSelect} + onClick={onClick} + variant={variant} {...rest} > @@ -66,13 +72,10 @@ export const RowButton = React.forwardRef( const StyledRowButtonInner = styled('div', { height: '100%', width: '100%', - color: '$text', - fontFamily: '$ui', - fontWeight: 400, - fontSize: '$1', backgroundColor: '$panel', borderRadius: '$2', display: 'flex', + gap: '$1', flexDirection: 'row', alignItems: 'center', padding: '0 $3', @@ -95,6 +98,10 @@ export const StyledRowButton = styled('button', { cursor: 'pointer', height: '32px', outline: 'none', + color: '$text', + fontFamily: '$ui', + fontWeight: 400, + fontSize: '$1', borderRadius: 4, userSelect: 'none', margin: 0, @@ -112,17 +119,33 @@ export const StyledRowButton = styled('button', { backgroundColor: '$hover', }, + '& a': { + textDecoration: 'none', + color: '$text', + }, + variants: { bp: { mobile: {}, small: {}, }, + variant: { + wide: { + gridColumn: '1 / span 4', + }, + }, size: { icon: { padding: '4px ', width: 'auto', }, }, + isSponsor: { + true: { + color: '#eb30a2', + }, + false: {}, + }, isWarning: { true: { color: '$warn', @@ -132,7 +155,29 @@ export const StyledRowButton = styled('button', { true: { backgroundColor: '$hover', }, - false: { + false: {}, + }, + }, + compoundVariants: [ + { + isActive: false, + isSponsor: true, + bp: 'small', + css: { + [`&:hover:not(:disabled) ${StyledRowButtonInner}`]: { + backgroundColor: '$sponsorContrast', + border: '1px solid $panel', + '& *[data-shy="true"]': { + opacity: 1, + }, + }, + }, + }, + { + isActive: false, + isSponsor: false, + bp: 'small', + css: { [`&:hover:not(:disabled) ${StyledRowButtonInner}`]: { backgroundColor: '$hover', border: '1px solid $panel', @@ -142,5 +187,5 @@ export const StyledRowButton = styled('button', { }, }, }, - }, + ], }) diff --git a/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx b/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx index bd1185aea..3a4699eb3 100644 --- a/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx +++ b/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx @@ -12,7 +12,7 @@ export const SmallIcon = styled('div', { border: 'none', pointerEvents: 'all', cursor: 'pointer', - color: '$text', + color: 'currentColor', '& svg': { height: 16, diff --git a/packages/tldraw/src/components/ToolButton/ToolButton.tsx b/packages/tldraw/src/components/ToolButton/ToolButton.tsx index f61315bf1..36be9e1c4 100644 --- a/packages/tldraw/src/components/ToolButton/ToolButton.tsx +++ b/packages/tldraw/src/components/ToolButton/ToolButton.tsx @@ -1,32 +1,52 @@ import * as React from 'react' import { breakpoints } from '~components/breakpoints' import { Tooltip } from '~components/Tooltip' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' import { styled } from '~styles' export interface ToolButtonProps { onClick?: () => void onSelect?: () => void onDoubleClick?: () => void + disabled?: boolean isActive?: boolean + isSponsor?: boolean + isToolLocked?: boolean variant?: 'icon' | 'text' | 'circle' | 'primary' children: React.ReactNode } export const ToolButton = React.forwardRef( - ({ onSelect, onClick, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => { + ( + { + onSelect, + onClick, + onDoubleClick, + variant, + children, + isToolLocked = false, + disabled = false, + isActive = false, + isSponsor = false, + ...rest + }, + ref + ) => { return ( {children} + {isToolLocked && } ) } @@ -36,20 +56,30 @@ export const ToolButton = React.forwardRef( interface ToolButtonWithTooltipProps extends ToolButtonProps { label: string + isLocked?: boolean kbd?: string } -export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) { - const { state } = useTLDrawContext() +export function ToolButtonWithTooltip({ + label, + kbd, + isLocked, + ...rest +}: ToolButtonWithTooltipProps) { + const app = useTldrawApp() const handleDoubleClick = React.useCallback(() => { - console.log('double clicking') - state.toggleToolLock() + app.toggleToolLock() }, []) return ( - + ) } @@ -112,6 +142,7 @@ export const StyledToolButton = styled('button', { circle: { padding: '$2', [`& ${StyledToolButtonInner}`]: { + border: '1px solid $panelContrast', borderRadius: '100%', boxShadow: '$panel', }, @@ -121,23 +152,17 @@ export const StyledToolButton = styled('button', { }, }, }, - isActive: { + isSponsor: { true: { [`${StyledToolButtonInner}`]: { - backgroundColor: '$selected', - color: '$panelActive', - }, - }, - false: { - [`&:hover:not(:disabled) ${StyledToolButtonInner}`]: { - backgroundColor: '$hover', - border: '1px solid $panel', - }, - [`&:focus:not(:disabled) ${StyledToolButtonInner}`]: { - backgroundColor: '$hover', + backgroundColor: '$sponsorContrast', }, }, }, + isActive: { + true: {}, + false: {}, + }, bp: { mobile: {}, small: {}, @@ -168,5 +193,40 @@ export const StyledToolButton = styled('button', { }, }, }, + { + isActive: true, + isSponsor: false, + css: { + [`${StyledToolButtonInner}`]: { + backgroundColor: '$selected', + color: '$selectedContrast', + }, + }, + }, + { + isActive: false, + isSponsor: false, + bp: 'small', + css: { + [`&:hover:not(:disabled) ${StyledToolButtonInner}`]: { + backgroundColor: '$hover', + border: '1px solid $panel', + }, + [`&:focus:not(:disabled) ${StyledToolButtonInner}`]: { + backgroundColor: '$hover', + }, + }, + }, ], }) + +const ToolLockIndicator = styled('div', { + position: 'absolute', + width: 10, + height: 10, + backgroundColor: '$selected', + borderRadius: '100%', + bottom: -2, + border: '2px solid $panel', + zIndex: 100, +}) diff --git a/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx b/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx index 11e448571..e45e6035b 100644 --- a/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx +++ b/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { Tooltip } from '~components/Tooltip/Tooltip' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' import { styled } from '~styles' -import { AlignType, TLDrawSnapshot, DistributeType, StretchType } from '~types' +import { AlignType, TDSnapshot, DistributeType, StretchType } from '~types' import { ArrowDownIcon, ArrowUpIcon, @@ -30,25 +30,24 @@ import { import { DMContent } from '~components/DropdownMenu' import { Divider } from '~components/Divider' import { TrashIcon } from '~components/icons' -import { IconButton } from '~components/IconButton' import { ToolButton } from '~components/ToolButton' -const selectedShapesCountSelector = (s: TLDrawSnapshot) => +const selectedShapesCountSelector = (s: TDSnapshot) => s.document.pageStates[s.appState.currentPageId].selectedIds.length -const isAllLockedSelector = (s: TLDrawSnapshot) => { +const isAllLockedSelector = (s: TDSnapshot) => { const page = s.document.pages[s.appState.currentPageId] const { selectedIds } = s.document.pageStates[s.appState.currentPageId] return selectedIds.every((id) => page.shapes[id].isLocked) } -const isAllAspectLockedSelector = (s: TLDrawSnapshot) => { +const isAllAspectLockedSelector = (s: TDSnapshot) => { const page = s.document.pages[s.appState.currentPageId] const { selectedIds } = s.document.pageStates[s.appState.currentPageId] return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked) } -const isAllGroupedSelector = (s: TLDrawSnapshot) => { +const isAllGroupedSelector = (s: TDSnapshot) => { const page = s.document.pages[s.appState.currentPageId] const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map( (id) => page.shapes[id] @@ -62,110 +61,110 @@ const isAllGroupedSelector = (s: TLDrawSnapshot) => { ) } -const hasSelectionClickor = (s: TLDrawSnapshot) => { +const hasSelectionClickor = (s: TDSnapshot) => { const { selectedIds } = s.document.pageStates[s.appState.currentPageId] return selectedIds.length > 0 } -const hasMultipleSelectionClickor = (s: TLDrawSnapshot) => { +const hasMultipleSelectionClickor = (s: TDSnapshot) => { const { selectedIds } = s.document.pageStates[s.appState.currentPageId] return selectedIds.length > 1 } export function ActionButton(): JSX.Element { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const isAllLocked = useSelector(isAllLockedSelector) + const isAllLocked = app.useStore(isAllLockedSelector) - const isAllAspectLocked = useSelector(isAllAspectLockedSelector) + const isAllAspectLocked = app.useStore(isAllAspectLockedSelector) - const isAllGrouped = useSelector(isAllGroupedSelector) + const isAllGrouped = app.useStore(isAllGroupedSelector) - const hasSelection = useSelector(hasSelectionClickor) + const hasSelection = app.useStore(hasSelectionClickor) - const hasMultipleSelection = useSelector(hasMultipleSelectionClickor) + const hasMultipleSelection = app.useStore(hasMultipleSelectionClickor) const handleRotate = React.useCallback(() => { - state.rotate() - }, [state]) + app.rotate() + }, [app]) const handleDuplicate = React.useCallback(() => { - state.duplicate() - }, [state]) + app.duplicate() + }, [app]) const handleToggleLocked = React.useCallback(() => { - state.toggleLocked() - }, [state]) + app.toggleLocked() + }, [app]) const handleToggleAspectRatio = React.useCallback(() => { - state.toggleAspectRatioLocked() - }, [state]) + app.toggleAspectRatioLocked() + }, [app]) const handleGroup = React.useCallback(() => { - state.group() - }, [state]) + app.group() + }, [app]) const handleMoveToBack = React.useCallback(() => { - state.moveToBack() - }, [state]) + app.moveToBack() + }, [app]) const handleMoveBackward = React.useCallback(() => { - state.moveBackward() - }, [state]) + app.moveBackward() + }, [app]) const handleMoveForward = React.useCallback(() => { - state.moveForward() - }, [state]) + app.moveForward() + }, [app]) const handleMoveToFront = React.useCallback(() => { - state.moveToFront() - }, [state]) + app.moveToFront() + }, [app]) const handleDelete = React.useCallback(() => { - state.delete() - }, [state]) + app.delete() + }, [app]) const alignTop = React.useCallback(() => { - state.align(AlignType.Top) - }, [state]) + app.align(AlignType.Top) + }, [app]) const alignCenterVertical = React.useCallback(() => { - state.align(AlignType.CenterVertical) - }, [state]) + app.align(AlignType.CenterVertical) + }, [app]) const alignBottom = React.useCallback(() => { - state.align(AlignType.Bottom) - }, [state]) + app.align(AlignType.Bottom) + }, [app]) const stretchVertically = React.useCallback(() => { - state.stretch(StretchType.Vertical) - }, [state]) + app.stretch(StretchType.Vertical) + }, [app]) const distributeVertically = React.useCallback(() => { - state.distribute(DistributeType.Vertical) - }, [state]) + app.distribute(DistributeType.Vertical) + }, [app]) const alignLeft = React.useCallback(() => { - state.align(AlignType.Left) - }, [state]) + app.align(AlignType.Left) + }, [app]) const alignCenterHorizontal = React.useCallback(() => { - state.align(AlignType.CenterHorizontal) - }, [state]) + app.align(AlignType.CenterHorizontal) + }, [app]) const alignRight = React.useCallback(() => { - state.align(AlignType.Right) - }, [state]) + app.align(AlignType.Right) + }, [app]) const stretchHorizontally = React.useCallback(() => { - state.stretch(StretchType.Horizontal) - }, [state]) + app.stretch(StretchType.Horizontal) + }, [app]) const distributeHorizontally = React.useCallback(() => { - state.distribute(DistributeType.Horizontal) - }, [state]) + app.distribute(DistributeType.Horizontal) + }, [app]) - const selectedShapesCount = useSelector(selectedShapesCountSelector) + const selectedShapesCount = app.useStore(selectedShapesCountSelector) const hasTwoOrMore = selectedShapesCount > 1 const hasThreeOrMore = selectedShapesCount > 2 @@ -177,99 +176,99 @@ export function ActionButton(): JSX.Element { - + <> - + - - + + - - + + {isAllLocked ? : } - - + + - - + - + - + - - + + - - + + - - + + - - + + - + - + - - + + - - + + - - + + - - + + - + - + - - + + - - + + - - + + - - + + - + diff --git a/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx b/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx index 3508ebeed..b08ab007c 100644 --- a/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx +++ b/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx @@ -1,24 +1,24 @@ import * as React from 'react' import { styled } from '~styles' -import type { TLDrawSnapshot } from '~types' -import { useTLDrawContext } from '~hooks' +import type { TDSnapshot } from '~types' +import { useTldrawApp } from '~hooks' import { RowButton } from '~components/RowButton' import { MenuContent } from '~components/MenuContent' -const isEmptyCanvasSelector = (s: TLDrawSnapshot) => +const isEmptyCanvasSelector = (s: TDSnapshot) => Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 && s.appState.isEmptyCanvas export const BackToContent = React.memo(function BackToContent() { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const isEmptyCanvas = useSelector(isEmptyCanvasSelector) + const isEmptyCanvas = app.useStore(isEmptyCanvasSelector) if (!isEmptyCanvas) return null return ( - Back to content + Back to content ) }) diff --git a/packages/tldraw/src/components/ToolsPanel/DeleteButton.tsx b/packages/tldraw/src/components/ToolsPanel/DeleteButton.tsx index 1be2aea6d..0bf580376 100644 --- a/packages/tldraw/src/components/ToolsPanel/DeleteButton.tsx +++ b/packages/tldraw/src/components/ToolsPanel/DeleteButton.tsx @@ -1,18 +1,18 @@ import * as React from 'react' import { Tooltip } from '~components/Tooltip' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' import { ToolButton } from '~components/ToolButton' import { TrashIcon } from '~components/icons' export function DeleteButton(): JSX.Element { - const { state } = useTLDrawContext() + const app = useTldrawApp() const handleDelete = React.useCallback(() => { - state.delete() - }, [state]) + app.delete() + }, [app]) return ( - + diff --git a/packages/tldraw/src/components/ToolsPanel/LockButton.tsx b/packages/tldraw/src/components/ToolsPanel/LockButton.tsx index 284c02a2b..57aa742f7 100644 --- a/packages/tldraw/src/components/ToolsPanel/LockButton.tsx +++ b/packages/tldraw/src/components/ToolsPanel/LockButton.tsx @@ -1,20 +1,20 @@ import * as React from 'react' import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons' import { Tooltip } from '~components/Tooltip' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' import { ToolButton } from '~components/ToolButton' -import type { TLDrawSnapshot } from '~types' +import type { TDSnapshot } from '~types' -const isToolLockedSelector = (s: TLDrawSnapshot) => s.appState.isToolLocked +const isToolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked export function LockButton(): JSX.Element { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const isToolLocked = useSelector(isToolLockedSelector) + const isToolLocked = app.useStore(isToolLockedSelector) return ( - + {isToolLocked ? : } diff --git a/packages/tldraw/src/components/ToolsPanel/PenMenu.tsx b/packages/tldraw/src/components/ToolsPanel/PenMenu.tsx new file mode 100644 index 000000000..dd3b09e77 --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/PenMenu.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { Panel } from '~components/Panel' +import { ToolButton } from '~components/ToolButton' +import { TDShapeType, TDToolType } from '~types' +import { useTldrawApp } from '~hooks' +import { Pencil1Icon } from '@radix-ui/react-icons' +import { Tooltip } from '~components/Tooltip' + +interface ShapesMenuProps { + activeTool: TDToolType +} + +type PenShape = TDShapeType.Draw +const penShapes: PenShape[] = [TDShapeType.Draw] +const penShapeIcons = { + [TDShapeType.Draw]: , +} + +export const PenMenu = React.memo(function PenMenu({ activeTool }: ShapesMenuProps) { + const app = useTldrawApp() + + const [lastActiveTool, setLastActiveTool] = React.useState(TDShapeType.Draw) + + React.useEffect(() => { + if (penShapes.includes(activeTool as PenShape) && lastActiveTool !== activeTool) { + setLastActiveTool(activeTool as PenShape) + } + }, [activeTool]) + + const selectShapeTool = React.useCallback(() => { + app.selectTool(lastActiveTool) + }, [activeTool, app]) + + const handleDoubleClick = React.useCallback(() => { + app.toggleToolLock() + }, []) + + return ( + + + + {penShapeIcons[lastActiveTool]} + + + + + {penShapes.map((shape, i) => ( + + + { + app.selectTool(shape) + setLastActiveTool(shape) + }} + > + {penShapeIcons[shape]} + + + + ))} + + + + ) +}) diff --git a/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx b/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx index 4d48bf8a9..9b4deaaed 100644 --- a/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx +++ b/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx @@ -1,52 +1,51 @@ import * as React from 'react' import { ArrowTopRightIcon, - CircleIcon, CursorArrowIcon, Pencil1Icon, Pencil2Icon, - SquareIcon, TextIcon, } from '@radix-ui/react-icons' -import { TLDrawSnapshot, TLDrawShapeType } from '~types' -import { useTLDrawContext } from '~hooks' +import { TDSnapshot, TDShapeType } from '~types' +import { useTldrawApp } from '~hooks' import { ToolButtonWithTooltip } from '~components/ToolButton' import { Panel } from '~components/Panel' +import { ShapesMenu } from './ShapesMenu' +import { EraserIcon } from '~components/icons' -const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool +const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool +const toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const activeTool = useSelector(activeToolSelector) + const activeTool = app.useStore(activeToolSelector) + + const isToolLocked = app.useStore(toolLockedSelector) const selectSelectTool = React.useCallback(() => { - state.selectTool('select') - }, [state]) + app.selectTool('select') + }, [app]) + + const selectEraseTool = React.useCallback(() => { + app.selectTool('erase') + }, [app]) const selectDrawTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Draw) - }, [state]) - - const selectRectangleTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Rectangle) - }, [state]) - - const selectEllipseTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Ellipse) - }, [state]) + app.selectTool(TDShapeType.Draw) + }, [app]) const selectArrowTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Arrow) - }, [state]) + app.selectTool(TDShapeType.Arrow) + }, [app]) const selectTextTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Text) - }, [state]) + app.selectTool(TDShapeType.Text) + }, [app]) const selectStickyTool = React.useCallback(() => { - state.selectTool(TLDrawShapeType.Sticky) - }, [state]) + app.selectTool(TDShapeType.Sticky) + }, [app]) return ( @@ -60,49 +59,43 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element { - + + - - - diff --git a/packages/tldraw/src/components/ToolsPanel/ShapesMenu.tsx b/packages/tldraw/src/components/ToolsPanel/ShapesMenu.tsx new file mode 100644 index 000000000..9c768a0c8 --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/ShapesMenu.tsx @@ -0,0 +1,83 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { Panel } from '~components/Panel' +import { ToolButton } from '~components/ToolButton' +import { TDShapeType, TDToolType } from '~types' +import { useTldrawApp } from '~hooks' +import { SquareIcon, CircleIcon } from '@radix-ui/react-icons' +import { Tooltip } from '~components/Tooltip' + +interface ShapesMenuProps { + activeTool: TDToolType + isToolLocked: boolean +} + +type ShapeShape = TDShapeType.Rectangle | TDShapeType.Ellipse +const shapeShapes: ShapeShape[] = [TDShapeType.Rectangle, TDShapeType.Ellipse] +const shapeShapeIcons = { + [TDShapeType.Rectangle]: , + [TDShapeType.Ellipse]: , +} + +export const ShapesMenu = React.memo(function ShapesMenu({ + activeTool, + isToolLocked, +}: ShapesMenuProps) { + const app = useTldrawApp() + + const [lastActiveTool, setLastActiveTool] = React.useState(TDShapeType.Rectangle) + + React.useEffect(() => { + if (shapeShapes.includes(activeTool as ShapeShape) && lastActiveTool !== activeTool) { + setLastActiveTool(activeTool as ShapeShape) + } + }, [activeTool]) + + const selectShapeTool = React.useCallback(() => { + app.selectTool(lastActiveTool) + }, [activeTool, app]) + + const handleDoubleClick = React.useCallback(() => { + app.toggleToolLock() + }, [app]) + + const isActive = shapeShapes.includes(activeTool as ShapeShape) + + return ( + + + + {shapeShapeIcons[lastActiveTool]} + + + + + {shapeShapes.map((shape, i) => ( + + + { + app.selectTool(shape) + setLastActiveTool(shape) + }} + > + {shapeShapeIcons[shape]} + + + + ))} + + + + ) +}) diff --git a/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx b/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx index 156e72e3f..5b6b6aae7 100644 --- a/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx +++ b/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx @@ -1,16 +1,16 @@ import * as React from 'react' -import { useTLDrawContext } from '~hooks' -import type { TLDrawSnapshot } from '~types' +import { useTldrawApp } from '~hooks' +import type { TDSnapshot } from '~types' import { styled } from '~styles' import { breakpoints } from '~components/breakpoints' -const statusSelector = (s: TLDrawSnapshot) => s.appState.status -const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool +const statusSelector = (s: TDSnapshot) => s.appState.status +const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool export function StatusBar(): JSX.Element | null { - const { useSelector } = useTLDrawContext() - const status = useSelector(statusSelector) - const activeTool = useSelector(activeToolSelector) + const app = useTldrawApp() + const status = app.useStore(statusSelector) + const activeTool = app.useStore(activeToolSelector) return ( @@ -24,7 +24,7 @@ export function StatusBar(): JSX.Element | null { const StyledStatusBar = styled('div', { height: 40, userSelect: 'none', - borderTop: '1px solid $border', + borderTop: '1px solid $panelContrast', gridArea: 'status', display: 'flex', color: '$text', diff --git a/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx index 829c59c3c..da628724e 100644 --- a/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx +++ b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx @@ -1,20 +1,19 @@ import * as React from 'react' import { styled } from '~styles' -import type { TLDrawSnapshot } from '~types' -import { useTLDrawContext } from '~hooks' +import type { TDSnapshot } from '~types' +import { useTldrawApp } from '~hooks' import { StatusBar } from './StatusBar' import { BackToContent } from './BackToContent' import { PrimaryTools } from './PrimaryTools' import { ActionButton } from './ActionButton' -import { LockButton } from './LockButton' import { DeleteButton } from './DeleteButton' -const isDebugModeSelector = (s: TLDrawSnapshot) => s.settings.isDebugMode +const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode export const ToolsPanel = React.memo(function ToolsPanel(): JSX.Element { - const { useSelector } = useTLDrawContext() + const app = useTldrawApp() - const isDebugMode = useSelector(isDebugModeSelector) + const isDebugMode = app.useStore(isDebugModeSelector) return ( @@ -48,6 +47,7 @@ const StyledToolsPanelContainer = styled('div', { gridTemplateRows: 'auto auto', justifyContent: 'space-between', padding: '0', + gap: '$4', zIndex: 200, pointerEvents: 'none', '& > div > *': { diff --git a/packages/tldraw/src/components/Tooltip/Tooltip.tsx b/packages/tldraw/src/components/Tooltip/Tooltip.tsx index ed8df37bc..e04be0c65 100644 --- a/packages/tldraw/src/components/Tooltip/Tooltip.tsx +++ b/packages/tldraw/src/components/Tooltip/Tooltip.tsx @@ -22,10 +22,10 @@ export function Tooltip({ }: TooltipProps): JSX.Element { return ( - + {children} - + {label} {kbdProp ? {kbdProp} : null} @@ -38,8 +38,8 @@ const StyledContent = styled(RadixTooltip.Content, { borderRadius: 3, padding: '$3 $3 $3 $3', fontSize: '$1', - backgroundColor: '$tooltipBg', - color: '$tooltipText', + backgroundColor: '$tooltip', + color: '$tooltipContrast', boxShadow: '$3', display: 'flex', alignItems: 'center', @@ -48,6 +48,6 @@ const StyledContent = styled(RadixTooltip.Content, { }) const StyledArrow = styled(RadixTooltip.Arrow, { - fill: '$tooltipBg', + fill: '$tooltip', margin: '0 8px', }) diff --git a/packages/tldraw/src/components/TopPanel/ColorMenu.tsx b/packages/tldraw/src/components/TopPanel/ColorMenu.tsx deleted file mode 100644 index 6032eba38..000000000 --- a/packages/tldraw/src/components/TopPanel/ColorMenu.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { strokes } from '~state/shapes/shape-styles' -import { useTLDrawContext } from '~hooks' -import { DMContent, DMTriggerIcon } from '~components/DropdownMenu' -import { BoxIcon, CircleIcon } from '~components/icons' -import { ToolButton } from '~components/ToolButton' -import type { TLDrawSnapshot, ColorStyle } from '~types' - -const selectColor = (s: TLDrawSnapshot) => s.appState.selectedStyle.color -const preventEvent = (e: Event) => e.preventDefault() -const themeSelector = (data: TLDrawSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light') - -export const ColorMenu = React.memo(function ColorMenu(): JSX.Element { - const { state, useSelector } = useTLDrawContext() - - const theme = useSelector(themeSelector) - const color = useSelector(selectColor) - - return ( - - - - - - {Object.keys(strokes[theme]).map((colorStyle: string) => ( - - state.style({ color: colorStyle as ColorStyle })} - > - - - - ))} - - - ) -}) diff --git a/packages/tldraw/src/components/TopPanel/DashMenu.tsx b/packages/tldraw/src/components/TopPanel/DashMenu.tsx deleted file mode 100644 index a12dc2aa3..000000000 --- a/packages/tldraw/src/components/TopPanel/DashMenu.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { useTLDrawContext } from '~hooks' -import { DashStyle, TLDrawSnapshot } from '~types' -import { DMContent, DMTriggerIcon } from '~components/DropdownMenu' -import { ToolButton } from '~components/ToolButton' -import { DashDashedIcon, DashDottedIcon, DashDrawIcon, DashSolidIcon } from '~components/icons' - -const dashes = { - [DashStyle.Draw]: , - [DashStyle.Solid]: , - [DashStyle.Dashed]: , - [DashStyle.Dotted]: , -} - -const selectDash = (s: TLDrawSnapshot) => s.appState.selectedStyle.dash - -const preventEvent = (e: Event) => e.preventDefault() - -export const DashMenu = React.memo(function DashMenu(): JSX.Element { - const { state, useSelector } = useTLDrawContext() - - const dash = useSelector(selectDash) - - return ( - - {dashes[dash]} - - {Object.values(DashStyle).map((dashStyle) => ( - - state.style({ dash: dashStyle as DashStyle })} - > - {dashes[dashStyle as DashStyle]} - - - ))} - - - ) -}) - -// function DashSolidIcon(): JSX.Element { -// return ( -// -// -// -// ) -// } - -// function DashDashedIcon(): JSX.Element { -// return ( -// -// -// -// ) -// } - -// const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` - -// function DashDottedIcon(): JSX.Element { -// return ( -// -// -// -// ) -// } - -// function DashDrawIcon(): JSX.Element { -// return ( -// -// -// -// ) -// } diff --git a/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx b/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx deleted file mode 100644 index c987cde0f..000000000 --- a/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import * as Checkbox from '@radix-ui/react-checkbox' -import { useTLDrawContext } from '~hooks' -import type { TLDrawSnapshot } from '~types' -import { BoxIcon, IsFilledIcon } from '~components/icons' -import { ToolButton } from '~components/ToolButton' - -const isFilledSelector = (s: TLDrawSnapshot) => s.appState.selectedStyle.isFilled - -export const FillCheckbox = React.memo(function FillCheckbox(): JSX.Element { - const { state, useSelector } = useTLDrawContext() - - const isFilled = useSelector(isFilledSelector) - - const handleIsFilledChange = React.useCallback( - (isFilled: boolean) => state.style({ isFilled }), - [state] - ) - - return ( - - - - - - - - - ) -}) diff --git a/packages/tldraw/src/components/TopPanel/HelpMenu.tsx b/packages/tldraw/src/components/TopPanel/HelpMenu.tsx new file mode 100644 index 000000000..7eafd865f --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/HelpMenu.tsx @@ -0,0 +1,3 @@ +export function HelpMenu() { + return
    +} diff --git a/packages/tldraw/src/components/TopPanel/Menu.tsx b/packages/tldraw/src/components/TopPanel/Menu.tsx index e44324a2f..ab936b339 100644 --- a/packages/tldraw/src/components/TopPanel/Menu.tsx +++ b/packages/tldraw/src/components/TopPanel/Menu.tsx @@ -1,90 +1,93 @@ import * as React from 'react' -import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons' +import { ExitIcon, GitHubLogoIcon, HamburgerMenuIcon, TwitterLogoIcon } from '@radix-ui/react-icons' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' import { PreferencesMenu } from './PreferencesMenu' import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu' import { SmallIcon } from '~components/SmallIcon' import { useFileSystemHandlers } from '~hooks' +import { HeartIcon } from '~components/icons/HeartIcon' +import { preventEvent } from '~components/preventEvent' interface MenuProps { + showSponsorLink: boolean readOnly: boolean } -export const Menu = React.memo(function Menu({ readOnly }: MenuProps) { - const { state } = useTLDrawContext() +export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) { + const app = useTldrawApp() const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers() const handleSignIn = React.useCallback(() => { - state.callbacks.onSignIn?.(state) - }, [state]) + app.callbacks.onSignIn?.(app) + }, [app]) const handleSignOut = React.useCallback(() => { - state.callbacks.onSignOut?.(state) - }, [state]) + app.callbacks.onSignOut?.(app) + }, [app]) const handleCut = React.useCallback(() => { - state.cut() - }, [state]) + app.cut() + }, [app]) const handleCopy = React.useCallback(() => { - state.copy() - }, [state]) + app.copy() + }, [app]) const handlePaste = React.useCallback(() => { - state.paste() - }, [state]) + app.paste() + }, [app]) const handleCopySvg = React.useCallback(() => { - state.copySvg() - }, [state]) + app.copySvg() + }, [app]) const handleCopyJson = React.useCallback(() => { - state.copyJson() - }, [state]) + app.copyJson() + }, [app]) const handleSelectAll = React.useCallback(() => { - state.selectAll() - }, [state]) + app.selectAll() + }, [app]) const handleselectNone = React.useCallback(() => { - state.selectNone() - }, [state]) + app.selectNone() + }, [app]) const showFileMenu = - state.callbacks.onNewProject || - state.callbacks.onOpenProject || - state.callbacks.onSaveProject || - state.callbacks.onSaveProjectAs + app.callbacks.onNewProject || + app.callbacks.onOpenProject || + app.callbacks.onSaveProject || + app.callbacks.onSaveProjectAs - const showSignInOutMenu = state.callbacks.onSignIn || state.callbacks.onSignOut + const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut || showSponsorLink return ( - - + + {showFileMenu && ( - {state.callbacks.onNewProject && ( - + {app.callbacks.onNewProject && ( + New Project )} - {state.callbacks.onOpenProject && ( - + {app.callbacks.onOpenProject && ( + Open... )} - {state.callbacks.onSaveProject && ( - + {app.callbacks.onSaveProject && ( + Save )} - {state.callbacks.onSaveProjectAs && ( - + {app.callbacks.onSaveProjectAs && ( + Save As... )} @@ -93,42 +96,76 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) { {!readOnly && ( <> - + Undo - + Redo - + Cut - + Copy - + Paste - + Copy as SVG - Copy as JSON + + Copy as JSON + - + Select All - Select None + + Select None + - )} + + Create a Multiplayer Room + + + + + + Github + + + + + + + + Twitter + + + + + + {showSponsorLink && ( + + + Become a Sponsor{' '} + + + + + + )} {showSignInOutMenu && ( <> {' '} - {state.callbacks.onSignIn && Sign In} - {state.callbacks.onSignOut && ( + {app.callbacks.onSignIn && Sign In} + {app.callbacks.onSignOut && ( Sign Out diff --git a/packages/tldraw/src/components/TopPanel/MultiplayerMenu.tsx b/packages/tldraw/src/components/TopPanel/MultiplayerMenu.tsx new file mode 100644 index 000000000..2cfbe931d --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/MultiplayerMenu.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import { CheckIcon, ClipboardIcon } from '@radix-ui/react-icons' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useTldrawApp } from '~hooks' +import { DMItem, DMContent, DMDivider, DMTriggerIcon } from '~components/DropdownMenu' +import { SmallIcon } from '~components/SmallIcon' +import { MultiplayerIcon } from '~components/icons' +import type { TDSnapshot } from '~types' +import { TLDR } from '~state/TLDR' + +const roomSelector = (state: TDSnapshot) => state.room + +export const MultiplayerMenu = React.memo(function MultiplayerMenu() { + const app = useTldrawApp() + + const room = app.useStore(roomSelector) + + const [copied, setCopied] = React.useState(false) + + const handleCopySelect = React.useCallback(() => { + setCopied(true) + TLDR.copyStringToClipboard(window.location.href) + setTimeout(() => setCopied(false), 1200) + }, []) + + const handleCreateMultiplayerRoom = React.useCallback(async () => { + if (app.isDirty) { + if (app.fileSystemHandle) { + if (window.confirm('Do you want to save changes to your current project?')) { + await app.saveProject() + } + } else { + if (window.confirm('Do you want to save your current project?')) { + await app.saveProject() + } + } + } else if (!app.fileSystemHandle) { + if (window.confirm('Do you want to save your current project?')) { + await app.saveProject() + } + } + }, []) + + const handleCopyToMultiplayerRoom = React.useCallback(async () => { + const myHeaders = new Headers({ + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }) + + const res = await fetch('http://tldraw.com/api/create-multiplayer-room', { + headers: myHeaders, + method: 'POST', + mode: 'cors', + cache: 'no-cache', + body: JSON.stringify(app.document), + }).then((res) => res.json()) + + window.location.href = `http://tldraw.com/r/${res.roomId}` + }, []) + + return ( + + + + + + + Create a Multiplayer Room + + Copy to Multiplayer Room + {room && ( + <> + + + Copy Invite Link{copied ? : } + + + )} + + + ) +}) diff --git a/packages/tldraw/src/components/TopPanel/PageMenu.tsx b/packages/tldraw/src/components/TopPanel/PageMenu.tsx index 0c3837cfd..4f04c42a2 100644 --- a/packages/tldraw/src/components/TopPanel/PageMenu.tsx +++ b/packages/tldraw/src/components/TopPanel/PageMenu.tsx @@ -3,23 +3,22 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { PlusIcon, CheckIcon } from '@radix-ui/react-icons' import { PageOptionsDialog } from './PageOptionsDialog' import { styled } from '~styles' -import { useTLDrawContext } from '~hooks' -import type { TLDrawSnapshot } from '~types' +import { useTldrawApp } from '~hooks' +import type { TDSnapshot } from '~types' import { DMContent, DMDivider } from '~components/DropdownMenu' import { SmallIcon } from '~components/SmallIcon' import { RowButton } from '~components/RowButton' import { ToolButton } from '~components/ToolButton' -const sortedSelector = (s: TLDrawSnapshot) => +const sortedSelector = (s: TDSnapshot) => Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) -const currentPageNameSelector = (s: TLDrawSnapshot) => - s.document.pages[s.appState.currentPageId].name +const currentPageNameSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].name -const currentPageIdSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId].id +const currentPageIdSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].id export function PageMenu(): JSX.Element { - const { useSelector } = useTLDrawContext() + const app = useTldrawApp() const rIsOpen = React.useRef(false) @@ -43,7 +42,7 @@ export function PageMenu(): JSX.Element { }, [setIsOpen] ) - const currentPageName = useSelector(currentPageNameSelector) + const currentPageName = app.useStore(currentPageNameSelector) return ( @@ -58,27 +57,27 @@ export function PageMenu(): JSX.Element { } function PageMenuContent({ onClose }: { onClose: () => void }) { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const sortedPages = useSelector(sortedSelector) + const sortedPages = app.useStore(sortedSelector) - const currentPageId = useSelector(currentPageIdSelector) + const currentPageId = app.useStore(currentPageIdSelector) const handleCreatePage = React.useCallback(() => { - state.createPage() - }, [state]) + app.createPage() + }, [app]) const handleChangePage = React.useCallback( (id: string) => { onClose() - state.changePage(id) + app.changePage(id) }, - [state] + [app] ) return ( <> - + {sortedPages.map((page) => ( { +const canDeleteSelector = (s: TDSnapshot) => { return Object.keys(s.document.pages).length > 1 } interface PageOptionsDialogProps { - page: TLDrawPage + page: TDPage onOpen?: () => void onClose?: () => void } export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() const [isOpen, setIsOpen] = React.useState(false) - const canDelete = useSelector(canDeleteSelector) + const canDelete = app.useStore(canDeleteSelector) const rInput = React.useRef(null) const handleDuplicate = React.useCallback(() => { - state.duplicatePage(page.id) + app.duplicatePage(page.id) onClose?.() - }, [state]) + }, [app]) const handleDelete = React.useCallback(() => { if (window.confirm(`Are you sure you want to delete this page?`)) { - state.deletePage(page.id) + app.deletePage(page.id) onClose?.() } - }, [state]) + }, [app]) const handleOpenChange = React.useCallback( (isOpen: boolean) => { @@ -50,7 +50,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr return } }, - [state, name] + [app] ) function stopPropagation(e: React.KeyboardEvent) { @@ -60,7 +60,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr // TODO: Replace with text input function handleRename() { const nextName = window.prompt('New name:', page.name) - state.renamePage(page.id, nextName || page.name || 'Page') + app.renamePage(page.id, nextName || page.name || 'Page') } React.useEffect(() => { @@ -82,7 +82,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr - + Rename Duplicate @@ -112,7 +112,6 @@ export const StyledDialogContent = styled(Dialog.Content, { marginTop: '-5vh', pointerEvents: 'all', backgroundColor: '$panel', - border: '1px solid $panelBorder', padding: '$0', borderRadius: '$2', font: '$ui', @@ -132,9 +131,9 @@ export const StyledDialogOverlay = styled(Dialog.Overlay, { height: '100%', }) -function DialogAction({ onSelect, ...rest }: RowButtonProps) { +function DialogAction({ ...rest }: RowButtonProps & { onSelect: (e: Event) => void }) { return ( - + ) diff --git a/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx b/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx index 046f9c124..faeec9460 100644 --- a/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx +++ b/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx @@ -1,42 +1,42 @@ import * as React from 'react' import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu' -import { useTLDrawContext } from '~hooks' -import type { TLDrawSnapshot } from '~types' +import { useTldrawApp } from '~hooks' +import type { TDSnapshot } from '~types' -const settingsSelector = (s: TLDrawSnapshot) => s.settings +const settingsSelector = (s: TDSnapshot) => s.settings export function PreferencesMenu() { - const { state, useSelector } = useTLDrawContext() + const app = useTldrawApp() - const settings = useSelector(settingsSelector) + const settings = app.useStore(settingsSelector) const toggleDebugMode = React.useCallback(() => { - state.setSetting('isDebugMode', (v) => !v) - }, [state]) + app.setSetting('isDebugMode', (v) => !v) + }, [app]) const toggleDarkMode = React.useCallback(() => { - state.setSetting('isDarkMode', (v) => !v) - }, [state]) + app.setSetting('isDarkMode', (v) => !v) + }, [app]) const toggleFocusMode = React.useCallback(() => { - state.setSetting('isFocusMode', (v) => !v) - }, [state]) + app.setSetting('isFocusMode', (v) => !v) + }, [app]) const toggleRotateHandle = React.useCallback(() => { - state.setSetting('showRotateHandles', (v) => !v) - }, [state]) + app.setSetting('showRotateHandles', (v) => !v) + }, [app]) const toggleBoundShapesHandle = React.useCallback(() => { - state.setSetting('showBindingHandles', (v) => !v) - }, [state]) + app.setSetting('showBindingHandles', (v) => !v) + }, [app]) const toggleisSnapping = React.useCallback(() => { - state.setSetting('isSnapping', (v) => !v) - }, [state]) + app.setSetting('isSnapping', (v) => !v) + }, [app]) const toggleCloneControls = React.useCallback(() => { - state.setSetting('showCloneHandles', (v) => !v) - }, [state]) + app.setSetting('showCloneHandles', (v) => !v) + }, [app]) return ( diff --git a/packages/tldraw/src/components/TopPanel/SizeMenu.tsx b/packages/tldraw/src/components/TopPanel/SizeMenu.tsx deleted file mode 100644 index 7b9bff5fd..000000000 --- a/packages/tldraw/src/components/TopPanel/SizeMenu.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { TLDrawSnapshot, SizeStyle } from '~types' -import { useTLDrawContext } from '~hooks' -import { DMContent, DMTriggerIcon } from '~components/DropdownMenu' -import { ToolButton } from '~components/ToolButton' -import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons' - -const sizes = { - [SizeStyle.Small]: , - [SizeStyle.Medium]: , - [SizeStyle.Large]: , -} - -const selectSize = (s: TLDrawSnapshot) => s.appState.selectedStyle.size - -const preventEvent = (e: Event) => e.preventDefault() - -export const SizeMenu = React.memo(function SizeMenu(): JSX.Element { - const { state, useSelector } = useTLDrawContext() - - const size = useSelector(selectSize) - - return ( - - {sizes[size as SizeStyle]} - - {Object.values(SizeStyle).map((sizeStyle: string) => ( - - state.style({ size: sizeStyle as SizeStyle })} - > - {sizes[sizeStyle as SizeStyle]} - - - ))} - - - ) -}) diff --git a/packages/tldraw/src/components/TopPanel/StyleMenu.tsx b/packages/tldraw/src/components/TopPanel/StyleMenu.tsx new file mode 100644 index 000000000..b8594d168 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/StyleMenu.tsx @@ -0,0 +1,209 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { strokes, fills } from '~state/shapes/shared/shape-styles' +import { useTldrawApp } from '~hooks' +import { DMCheckboxItem, DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu' +import { + CircleIcon, + DashDashedIcon, + DashDottedIcon, + DashDrawIcon, + DashSolidIcon, + SizeLargeIcon, + SizeMediumIcon, + SizeSmallIcon, +} from '~components/icons' +import { ToolButton } from '~components/ToolButton' +import { TDSnapshot, ColorStyle, DashStyle, SizeStyle } from '~types' +import { styled } from '~styles' +import { breakpoints } from '~components/breakpoints' +import { Divider } from '~components/Divider' +import { preventEvent } from '~components/preventEvent' + +const selectedStyleSelector = (s: TDSnapshot) => s.appState.selectedStyle + +const dashes = { + [DashStyle.Draw]: , + [DashStyle.Solid]: , + [DashStyle.Dashed]: , + [DashStyle.Dotted]: , +} + +const sizes = { + [SizeStyle.Small]: , + [SizeStyle.Medium]: , + [SizeStyle.Large]: , +} + +const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light') + +export const StyleMenu = React.memo(function ColorMenu(): JSX.Element { + const app = useTldrawApp() + + const theme = app.useStore(themeSelector) + + const style = app.useStore(selectedStyleSelector) + + const handleToggleFilled = React.useCallback((checked: boolean) => { + app.style({ isFilled: checked }) + }, []) + + const handleDashChange = React.useCallback((value: string) => { + app.style({ dash: value as DashStyle }) + }, []) + + const handleSizeChange = React.useCallback((value: string) => { + app.style({ size: value as SizeStyle }) + }, []) + + return ( + + + + {style.isFilled && ( + + )} + {dashes[style.dash]} + + + + + Color + + {Object.keys(strokes.light).map((colorStyle: string) => ( + + app.style({ color: colorStyle as ColorStyle })} + > + + + + ))} + + + + + Dash + + {Object.values(DashStyle).map((dashStyle) => ( + + {dashes[dashStyle as DashStyle]} + + ))} + + + + + Size + + {Object.values(SizeStyle).map((sizeStyle) => ( + + {sizes[sizeStyle as SizeStyle]} + + ))} + + + + + Fill + + + + ) +}) + +const ColorGrid = styled('div', { + display: 'grid', + gridTemplateColumns: 'repeat(4, auto)', + gap: 0, +}) + +// const StyledRowInner = styled('div', { +// height: '100%', +// width: '100%', +// backgroundColor: '$panel', +// borderRadius: '$2', +// display: 'flex', +// gap: '$1', +// flexDirection: 'row', +// alignItems: 'center', +// padding: '0 $3', +// justifyContent: 'space-between', +// border: '1px solid transparent', + +// '& svg': { +// position: 'relative', +// stroke: '$overlay', +// strokeWidth: 1, +// zIndex: 1, +// }, +// }) + +export const StyledRow = styled('div', { + position: 'relative', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + minHeight: '32px', + outline: 'none', + color: '$text', + fontFamily: '$ui', + fontWeight: 400, + fontSize: '$1', + padding: '0 0 0 $3', + borderRadius: 4, + userSelect: 'none', + margin: 0, + display: 'flex', + gap: '$3', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + variants: { + variant: { + tall: { + alignItems: 'flex-start', + '& > span': { + paddingTop: '$4', + }, + }, + }, + }, +}) + +const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, { + display: 'flex', + flexDirection: 'row', +}) + +const OverlapIcons = styled('div', { + display: 'grid', + '& > *': { + gridColumn: 1, + gridRow: 1, + }, +}) diff --git a/packages/tldraw/src/components/TopPanel/TopPanel.tsx b/packages/tldraw/src/components/TopPanel/TopPanel.tsx index 8a62281c0..f7aac521a 100644 --- a/packages/tldraw/src/components/TopPanel/TopPanel.tsx +++ b/packages/tldraw/src/components/TopPanel/TopPanel.tsx @@ -3,10 +3,7 @@ import { Menu } from './Menu' import { styled } from '~styles' import { PageMenu } from './PageMenu' import { ZoomMenu } from './ZoomMenu' -import { DashMenu } from './DashMenu' -import { SizeMenu } from './SizeMenu' -import { FillCheckbox } from './FillCheckbox' -import { ColorMenu } from './ColorMenu' +import { StyleMenu } from './StyleMenu' import { Panel } from '~components/Panel' interface TopPanelProps { @@ -15,28 +12,29 @@ interface TopPanelProps { showMenu: boolean showStyles: boolean showZoom: boolean + showSponsorLink: boolean } -export function TopPanel({ readOnly, showPages, showMenu, showStyles, showZoom }: TopPanelProps) { +export function TopPanel({ + readOnly, + showPages, + showMenu, + showStyles, + showZoom, + showSponsorLink, +}: TopPanelProps) { return ( {(showMenu || showPages) && ( - {showMenu && } + {showMenu && } {showPages && } )} {(showStyles || showZoom) && ( - {showStyles && !readOnly && ( - <> - - - - - - )} + {showStyles && !readOnly && } {showZoom && } )} diff --git a/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx b/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx index 6243b37e2..e65152588 100644 --- a/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx +++ b/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx @@ -1,43 +1,46 @@ import * as React from 'react' -import { useTLDrawContext } from '~hooks' -import type { TLDrawSnapshot } from '~types' +import { useTldrawApp } from '~hooks' +import type { TDSnapshot } from '~types' import { styled } from '~styles' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { DMItem, DMContent } from '~components/DropdownMenu' import { ToolButton } from '~components/ToolButton' +import { preventEvent } from '~components/preventEvent' -const zoomSelector = (s: TLDrawSnapshot) => - s.document.pageStates[s.appState.currentPageId].camera.zoom +const zoomSelector = (s: TDSnapshot) => s.document.pageStates[s.appState.currentPageId].camera.zoom -export function ZoomMenu() { - const { state, useSelector } = useTLDrawContext() - const zoom = useSelector(zoomSelector) +export const ZoomMenu = React.memo(function ZoomMenu() { + const app = useTldrawApp() + + const zoom = app.useStore(zoomSelector) return ( - - - {Math.round(zoom * 100)}% + + + + {Math.round(zoom * 100)}% + - + Zoom In - + Zoom Out - + To 100% - + To Fit - + To Selection ) -} +}) const FixedWidthToolButton = styled(ToolButton, { minWidth: 56, diff --git a/packages/tldraw/src/components/icons/BoxIcon.tsx b/packages/tldraw/src/components/icons/BoxIcon.tsx index 4a3937d0f..9ca390262 100644 --- a/packages/tldraw/src/components/icons/BoxIcon.tsx +++ b/packages/tldraw/src/components/icons/BoxIcon.tsx @@ -3,9 +3,11 @@ import * as React from 'react' export function BoxIcon({ fill = 'none', stroke = 'currentColor', + strokeWidth = 2, }: { fill?: string stroke?: string + strokeWidth?: number }): JSX.Element { return ( - + ) } diff --git a/packages/tldraw/src/components/icons/CircleIcon.tsx b/packages/tldraw/src/components/icons/CircleIcon.tsx index e1f64c5e5..55bb04557 100644 --- a/packages/tldraw/src/components/icons/CircleIcon.tsx +++ b/packages/tldraw/src/components/icons/CircleIcon.tsx @@ -1,7 +1,7 @@ import * as React from 'react' export function CircleIcon( - props: Pick, 'stroke' | 'fill'> & { + props: Pick, 'strokeWidth' | 'stroke' | 'fill'> & { size: number } ) { diff --git a/packages/tldraw/src/components/icons/EraserIcon.tsx b/packages/tldraw/src/components/icons/EraserIcon.tsx new file mode 100644 index 000000000..9d7e3b5b9 --- /dev/null +++ b/packages/tldraw/src/components/icons/EraserIcon.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +export function EraserIcon(): JSX.Element { + return ( + + + + + + ) +} diff --git a/packages/tldraw/src/components/icons/HeartIcon.tsx b/packages/tldraw/src/components/icons/HeartIcon.tsx new file mode 100644 index 000000000..addeb5976 --- /dev/null +++ b/packages/tldraw/src/components/icons/HeartIcon.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +export function HeartIcon() { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/IsFilledIcon.tsx b/packages/tldraw/src/components/icons/IsFilledIcon.tsx index 9efd8429d..10cb8591b 100644 --- a/packages/tldraw/src/components/icons/IsFilledIcon.tsx +++ b/packages/tldraw/src/components/icons/IsFilledIcon.tsx @@ -11,7 +11,7 @@ export function IsFilledIcon(): JSX.Element { rx="2" strokeWidth="2" fill="currentColor" - opacity=".3" + opacity=".9" /> ) diff --git a/packages/tldraw/src/components/icons/MultiplayerIcon.tsx b/packages/tldraw/src/components/icons/MultiplayerIcon.tsx new file mode 100644 index 000000000..99450288e --- /dev/null +++ b/packages/tldraw/src/components/icons/MultiplayerIcon.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' + +export function MultiplayerIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/index.ts b/packages/tldraw/src/components/icons/index.ts index aeeb1685a..7acf98d6e 100644 --- a/packages/tldraw/src/components/icons/index.ts +++ b/packages/tldraw/src/components/icons/index.ts @@ -11,3 +11,5 @@ export * from './UndoIcon' export * from './SizeSmallIcon' export * from './SizeMediumIcon' export * from './SizeLargeIcon' +export * from './EraserIcon' +export * from './MultiplayerIcon' diff --git a/packages/tldraw/src/components/preventEvent.ts b/packages/tldraw/src/components/preventEvent.ts new file mode 100644 index 000000000..48e932546 --- /dev/null +++ b/packages/tldraw/src/components/preventEvent.ts @@ -0,0 +1 @@ +export const preventEvent = (e: Event) => e.preventDefault() diff --git a/packages/tldraw/src/components/stopPropagation.ts b/packages/tldraw/src/components/stopPropagation.ts new file mode 100644 index 000000000..a04836752 --- /dev/null +++ b/packages/tldraw/src/components/stopPropagation.ts @@ -0,0 +1 @@ +export const stopPropagation = (e: Event) => e.stopPropagation() diff --git a/packages/tldraw/src/constants.ts b/packages/tldraw/src/constants.ts index 35b542eb7..db0aed101 100644 --- a/packages/tldraw/src/constants.ts +++ b/packages/tldraw/src/constants.ts @@ -6,6 +6,8 @@ export const SNAP_DISTANCE = 5 export const EMPTY_ARRAY = [] as any[] export const SLOW_SPEED = 10 export const VERY_SLOW_SPEED = 2.5 +export const GHOSTED_OPACITY = 0.3 +export const DEAD_ZONE = 3 import type { Easing } from '~types' diff --git a/packages/tldraw/src/hooks/index.ts b/packages/tldraw/src/hooks/index.ts index 226eb59da..74bc24099 100644 --- a/packages/tldraw/src/hooks/index.ts +++ b/packages/tldraw/src/hooks/index.ts @@ -1,5 +1,5 @@ export * from './useKeyboardShortcuts' -export * from './useTLDrawContext' +export * from './useTldrawApp' export * from './useTheme' export * from './useStylesheet' export * from './useFileSystemHandlers' diff --git a/packages/tldraw/src/hooks/useFileSystem.ts b/packages/tldraw/src/hooks/useFileSystem.ts index 904d21148..99ee9ba39 100644 --- a/packages/tldraw/src/hooks/useFileSystem.ts +++ b/packages/tldraw/src/hooks/useFileSystem.ts @@ -1,41 +1,41 @@ import * as React from 'react' -import type { TLDrawState } from '~state' +import type { TldrawApp } from '~state' export function useFileSystem() { - const promptSaveBeforeChange = React.useCallback(async (state: TLDrawState) => { - if (state.isDirty) { - if (state.fileSystemHandle) { + const promptSaveBeforeChange = React.useCallback(async (app: TldrawApp) => { + if (app.isDirty) { + if (app.fileSystemHandle) { if (window.confirm('Do you want to save changes to your current project?')) { - await state.saveProject() + await app.saveProject() } } else { if (window.confirm('Do you want to save your current project?')) { - await state.saveProject() + await app.saveProject() } } } }, []) const onNewProject = React.useCallback( - async (state: TLDrawState) => { - await promptSaveBeforeChange(state) - state.newProject() + async (app: TldrawApp) => { + await promptSaveBeforeChange(app) + app.newProject() }, [promptSaveBeforeChange] ) - const onSaveProject = React.useCallback((state: TLDrawState) => { - state.saveProject() + const onSaveProject = React.useCallback((app: TldrawApp) => { + app.saveProject() }, []) - const onSaveProjectAs = React.useCallback((state: TLDrawState) => { - state.saveProjectAs() + const onSaveProjectAs = React.useCallback((app: TldrawApp) => { + app.saveProjectAs() }, []) const onOpenProject = React.useCallback( - async (state: TLDrawState) => { - await promptSaveBeforeChange(state) - state.openProject() + async (app: TldrawApp) => { + await promptSaveBeforeChange(app) + app.openProject() }, [promptSaveBeforeChange] ) diff --git a/packages/tldraw/src/hooks/useFileSystemHandlers.ts b/packages/tldraw/src/hooks/useFileSystemHandlers.ts index 00b6c2a04..a8717b542 100644 --- a/packages/tldraw/src/hooks/useFileSystemHandlers.ts +++ b/packages/tldraw/src/hooks/useFileSystemHandlers.ts @@ -1,39 +1,39 @@ import * as React from 'react' -import { useTLDrawContext } from '~hooks' +import { useTldrawApp } from '~hooks' export function useFileSystemHandlers() { - const { state } = useTLDrawContext() + const app = useTldrawApp() const onNewProject = React.useCallback( - async (e?: KeyboardEvent) => { - if (e && state.callbacks.onOpenProject) e.preventDefault() - state.callbacks.onNewProject?.(state) + async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => { + if (e && app.callbacks.onOpenProject) e.preventDefault() + app.callbacks.onNewProject?.(app) }, - [state] + [app] ) const onSaveProject = React.useCallback( - (e?: KeyboardEvent) => { - if (e && state.callbacks.onOpenProject) e.preventDefault() - state.callbacks.onSaveProject?.(state) + (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => { + if (e && app.callbacks.onOpenProject) e.preventDefault() + app.callbacks.onSaveProject?.(app) }, - [state] + [app] ) const onSaveProjectAs = React.useCallback( - (e?: KeyboardEvent) => { - if (e && state.callbacks.onOpenProject) e.preventDefault() - state.callbacks.onSaveProjectAs?.(state) + (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => { + if (e && app.callbacks.onOpenProject) e.preventDefault() + app.callbacks.onSaveProjectAs?.(app) }, - [state] + [app] ) const onOpenProject = React.useCallback( - async (e?: KeyboardEvent) => { - if (e && state.callbacks.onOpenProject) e.preventDefault() - state.callbacks.onOpenProject?.(state) + async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => { + if (e && app.callbacks.onOpenProject) e.preventDefault() + app.callbacks.onOpenProject?.(app) }, - [state] + [app] ) return { diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx index 071623513..bee1f74ed 100644 --- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx +++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import { TLDrawShapeType } from '~types' -import { useFileSystemHandlers, useTLDrawContext } from '~hooks' +import { TDShapeType } from '~types' +import { useFileSystemHandlers, useTldrawApp } from '~hooks' export function useKeyboardShortcuts(ref: React.RefObject) { - const { state } = useTLDrawContext() + const app = useTldrawApp() const canHandleEvent = React.useCallback(() => { const elm = ref.current @@ -16,63 +16,80 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'v,1', () => { - if (canHandleEvent()) state.selectTool('select') + if (!canHandleEvent()) return + app.selectTool('select') }, - [state, ref.current] + [app, ref.current] ) useHotkeys( 'd,2', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Draw) + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Draw) }, undefined, - [state] + [app] ) useHotkeys( - 'r,3', + 'e,3', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Rectangle) + if (!canHandleEvent()) return + app.selectTool('erase') }, undefined, - [state] + [app] ) useHotkeys( - 'e,4', + 'r,4', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Ellipse) + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Rectangle) }, undefined, - [state] + [app] ) useHotkeys( - 'a,5', + '5', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Arrow) + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Ellipse) }, undefined, - [state] + [app] ) useHotkeys( - 't,6', + 'a,6', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Text) + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Arrow) }, undefined, - [state] + [app] ) useHotkeys( - 'n,7', + 't,7', () => { - if (canHandleEvent()) state.selectTool(TLDrawShapeType.Sticky) + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Text) }, undefined, - [state] + [app] + ) + + useHotkeys( + 'n,8', + () => { + if (!canHandleEvent()) return + app.selectTool(TDShapeType.Sticky) + }, + undefined, + [app] ) /* ---------------------- Misc ---------------------- */ @@ -82,13 +99,12 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'ctrl+shift+d,command+shift+d', (e) => { - if (canHandleEvent()) { - state.toggleDarkMode() - e.preventDefault() - } + if (!canHandleEvent()) return + app.toggleDarkMode() + e.preventDefault() }, undefined, - [state] + [app] ) // Focus Mode @@ -96,10 +112,11 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'ctrl+.,command+.', () => { - if (canHandleEvent()) state.toggleFocusMode() + if (!canHandleEvent()) return + app.toggleFocusMode() }, undefined, - [state] + [app] ) // File System @@ -109,43 +126,43 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'ctrl+n,command+n', (e) => { - if (canHandleEvent()) { - onNewProject(e) - } + if (!canHandleEvent()) return + + onNewProject(e) }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+s,command+s', (e) => { - if (canHandleEvent()) { - onSaveProject(e) - } + if (!canHandleEvent()) return + + onSaveProject(e) }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+shift+s,command+shift+s', (e) => { - if (canHandleEvent()) { - onSaveProjectAs(e) - } + if (!canHandleEvent()) return + + onSaveProjectAs(e) }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+o,command+o', (e) => { - if (canHandleEvent()) { - onOpenProject(e) - } + if (!canHandleEvent()) return + + onOpenProject(e) }, undefined, - [state] + [app] ) // Undo Redo @@ -153,31 +170,31 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'command+z,ctrl+z', () => { - if (canHandleEvent()) { - if (state.session) { - state.cancelSession() - } else { - state.undo() - } + if (!canHandleEvent()) return + + if (app.session) { + app.cancelSession() + } else { + app.undo() } }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+shift-z,command+shift+z', () => { - if (canHandleEvent()) { - if (state.session) { - state.cancelSession() - } else { - state.redo() - } + if (!canHandleEvent()) return + + if (app.session) { + app.cancelSession() + } else { + app.redo() } }, undefined, - [state] + [app] ) // Undo Redo @@ -185,19 +202,21 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'command+u,ctrl+u', () => { - if (canHandleEvent()) state.undoSelect() + if (!canHandleEvent()) return + app.undoSelect() }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+shift-u,command+shift+u', () => { - if (canHandleEvent()) state.redoSelect() + if (!canHandleEvent()) return + app.redoSelect() }, undefined, - [state] + [app] ) /* -------------------- Commands -------------------- */ @@ -207,52 +226,55 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'ctrl+=,command+=', (e) => { - if (canHandleEvent()) { - state.zoomIn() - e.preventDefault() - } + if (!canHandleEvent()) return + + app.zoomIn() + e.preventDefault() }, undefined, - [state] + [app] ) useHotkeys( 'ctrl+-,command+-', (e) => { - if (canHandleEvent()) { - state.zoomOut() - e.preventDefault() - } + if (!canHandleEvent()) return + + app.zoomOut() + e.preventDefault() }, undefined, - [state] + [app] ) useHotkeys( 'shift+1', () => { - if (canHandleEvent()) state.zoomToFit() + if (!canHandleEvent()) return + app.zoomToFit() }, undefined, - [state] + [app] ) useHotkeys( 'shift+2', () => { - if (canHandleEvent()) state.zoomToSelection() + if (!canHandleEvent()) return + app.zoomToSelection() }, undefined, - [state] + [app] ) useHotkeys( 'shift+0', () => { - if (canHandleEvent()) state.resetZoom() + if (!canHandleEvent()) return + app.resetZoom() }, undefined, - [state] + [app] ) // Duplicate @@ -260,13 +282,13 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'ctrl+d,command+d', (e) => { - if (canHandleEvent()) { - state.duplicate() - e.preventDefault() - } + if (!canHandleEvent()) return + + app.duplicate() + e.preventDefault() }, undefined, - [state] + [app] ) // Flip @@ -274,19 +296,21 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'shift+h', () => { - if (canHandleEvent()) state.flipHorizontal() + if (!canHandleEvent()) return + app.flipHorizontal() }, undefined, - [state] + [app] ) useHotkeys( 'shift+v', () => { - if (canHandleEvent()) state.flipVertical() + if (!canHandleEvent()) return + app.flipVertical() }, undefined, - [state] + [app] ) // Cancel @@ -294,12 +318,12 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'escape', () => { - if (canHandleEvent()) { - state.cancel() - } + if (!canHandleEvent()) return + + app.cancel() }, undefined, - [state] + [app] ) // Delete @@ -307,10 +331,11 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'backspace', () => { - if (canHandleEvent()) state.delete() + if (!canHandleEvent()) return + app.delete() }, undefined, - [state] + [app] ) // Select All @@ -318,10 +343,11 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'command+a,ctrl+a', () => { - if (canHandleEvent()) state.selectAll() + if (!canHandleEvent()) return + app.selectAll() }, undefined, - [state] + [app] ) // Nudge @@ -329,73 +355,91 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'up', () => { - if (canHandleEvent()) state.nudge([0, -1], false) + if (!canHandleEvent()) return + app.nudge([0, -1], false) }, undefined, - [state] + [app] ) useHotkeys( 'right', () => { - if (canHandleEvent()) state.nudge([1, 0], false) + if (!canHandleEvent()) return + app.nudge([1, 0], false) }, undefined, - [state] + [app] ) useHotkeys( 'down', () => { - if (canHandleEvent()) state.nudge([0, 1], false) + if (!canHandleEvent()) return + app.nudge([0, 1], false) }, undefined, - [state] + [app] ) useHotkeys( 'left', () => { - if (canHandleEvent()) state.nudge([-1, 0], false) + if (!canHandleEvent()) return + app.nudge([-1, 0], false) }, undefined, - [state] + [app] ) useHotkeys( 'shift+up', () => { - if (canHandleEvent()) state.nudge([0, -1], true) + if (!canHandleEvent()) return + app.nudge([0, -1], true) }, undefined, - [state] + [app] ) useHotkeys( 'shift+right', () => { - if (canHandleEvent()) state.nudge([1, 0], true) + if (!canHandleEvent()) return + app.nudge([1, 0], true) }, undefined, - [state] + [app] ) useHotkeys( 'shift+down', () => { - if (canHandleEvent()) state.nudge([0, 1], true) + if (!canHandleEvent()) return + app.nudge([0, 1], true) }, undefined, - [state] + [app] ) useHotkeys( 'shift+left', () => { - if (canHandleEvent()) state.nudge([-1, 0], true) + if (!canHandleEvent()) return + app.nudge([-1, 0], true) }, undefined, - [state] + [app] + ) + + useHotkeys( + 'command+shift+l,ctrl+shift+l', + () => { + if (!canHandleEvent()) return + app.toggleLocked() + }, + undefined, + [app] ) // Copy, Cut & Paste @@ -403,28 +447,31 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'command+c,ctrl+c', () => { - if (canHandleEvent()) state.copy() + if (!canHandleEvent()) return + app.copy() }, undefined, - [state] + [app] ) useHotkeys( 'command+x,ctrl+x', () => { - if (canHandleEvent()) state.cut() + if (!canHandleEvent()) return + app.cut() }, undefined, - [state] + [app] ) useHotkeys( 'command+v,ctrl+v', () => { - if (canHandleEvent()) state.paste() + if (!canHandleEvent()) return + app.paste() }, undefined, - [state] + [app] ) // Group & Ungroup @@ -432,25 +479,25 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'command+g,ctrl+g', (e) => { - if (canHandleEvent()) { - state.group() - e.preventDefault() - } + if (!canHandleEvent()) return + + app.group() + e.preventDefault() }, undefined, - [state] + [app] ) useHotkeys( 'command+shift+g,ctrl+shift+g', (e) => { - if (canHandleEvent()) { - state.ungroup() - e.preventDefault() - } + if (!canHandleEvent()) return + + app.ungroup() + e.preventDefault() }, undefined, - [state] + [app] ) // Move @@ -458,50 +505,53 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( '[', () => { - if (canHandleEvent()) state.moveBackward() + if (!canHandleEvent()) return + app.moveBackward() }, undefined, - [state] + [app] ) useHotkeys( ']', () => { - if (canHandleEvent()) state.moveForward() + if (!canHandleEvent()) return + app.moveForward() }, undefined, - [state] + [app] ) useHotkeys( 'shift+[', () => { - if (canHandleEvent()) state.moveToBack() + if (!canHandleEvent()) return + app.moveToBack() }, undefined, - [state] + [app] ) useHotkeys( 'shift+]', () => { - if (canHandleEvent()) state.moveToFront() + if (!canHandleEvent()) return + app.moveToFront() }, undefined, - [state] + [app] ) useHotkeys( 'command+shift+backspace', (e) => { - if (canHandleEvent()) { - if (process.env.NODE_ENV === 'development') { - state.resetDocument() - } - e.preventDefault() + if (!canHandleEvent()) return + if (app.settings.isDebugMode) { + app.resetDocument() } + e.preventDefault() }, undefined, - [state] + [app] ) } diff --git a/packages/tldraw/src/hooks/useStylesheet.ts b/packages/tldraw/src/hooks/useStylesheet.ts index e5c154a95..3806c99b2 100644 --- a/packages/tldraw/src/hooks/useStylesheet.ts +++ b/packages/tldraw/src/hooks/useStylesheet.ts @@ -2,8 +2,10 @@ import * as React from 'react' const styles = new Map() -const UID = `tldraw-fonts` -const CSS = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')` +const UID = `Tldraw-fonts` +const CSS = ` +@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap') +` export function useStylesheet() { React.useLayoutEffect(() => { diff --git a/packages/tldraw/src/hooks/useTLDrawContext.tsx b/packages/tldraw/src/hooks/useTLDrawContext.tsx deleted file mode 100644 index cd57c6ae3..000000000 --- a/packages/tldraw/src/hooks/useTLDrawContext.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react' -import type { TLDrawSnapshot } from '~types' -import type { UseBoundStore } from 'zustand' -import type { TLDrawState } from '~state' - -export interface TLDrawContextType { - state: TLDrawState - useSelector: UseBoundStore -} - -export const TLDrawContext = React.createContext({} as TLDrawContextType) - -export function useTLDrawContext() { - const context = React.useContext(TLDrawContext) - - return context -} diff --git a/packages/tldraw/src/hooks/useTheme.ts b/packages/tldraw/src/hooks/useTheme.ts index 63e59a3fe..0708cda1c 100644 --- a/packages/tldraw/src/hooks/useTheme.ts +++ b/packages/tldraw/src/hooks/useTheme.ts @@ -1,14 +1,14 @@ -import type { TLDrawSnapshot, Theme } from '~types' -import { useTLDrawContext } from './useTLDrawContext' +import type { TDSnapshot, Theme } from '~types' +import { useTldrawApp } from './useTldrawApp' -const themeSelector = (data: TLDrawSnapshot): Theme => (data.settings.isDarkMode ? 'dark' : 'light') +const themeSelector = (data: TDSnapshot): Theme => (data.settings.isDarkMode ? 'dark' : 'light') export function useTheme() { - const { state, useSelector } = useTLDrawContext() - const theme = useSelector(themeSelector) + const app = useTldrawApp() + const theme = app.useStore(themeSelector) return { theme, - toggle: state.toggleDarkMode, + toggle: app.toggleDarkMode, } } diff --git a/packages/tldraw/src/hooks/useTldrawApp.tsx b/packages/tldraw/src/hooks/useTldrawApp.tsx new file mode 100644 index 000000000..92b5f9872 --- /dev/null +++ b/packages/tldraw/src/hooks/useTldrawApp.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import type { TldrawApp } from '~state' + +export const TldrawContext = React.createContext({} as TldrawApp) + +export function useTldrawApp() { + const context = React.useContext(TldrawContext) + + return context +} diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index 75fa44917..42fbc3671 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -1,5 +1,5 @@ -export * from './TLDraw' +export * from './Tldraw' export * from './types' export * from './state/shapes' -export { TLDrawState } from './state' +export { TldrawApp } from './state' export { useFileSystem } from './hooks' diff --git a/packages/tldraw/src/state/TLDR.ts b/packages/tldraw/src/state/TLDR.ts index 8be397abb..ad0693f73 100644 --- a/packages/tldraw/src/state/TLDR.ts +++ b/packages/tldraw/src/state/TLDR.ts @@ -1,36 +1,36 @@ import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core' import { - TLDrawSnapshot, + TDSnapshot, ShapeStyles, ShapesWithProp, - TLDrawShape, - TLDrawBinding, - TLDrawPage, - TLDrawCommand, - TLDrawPatch, - TLDrawShapeType, + TDShape, + TDBinding, + TDPage, + TldrawCommand, + TldrawPatch, + TDShapeType, ArrowShape, } from '~types' import { Vec } from '@tldraw/vec' -import type { TLDrawShapeUtil } from './shapes/TLDrawShapeUtil' -import { getShapeUtils } from './shapes' +import type { TDShapeUtil } from './shapes/TDShapeUtil' +import { getShapeUtil } from './shapes' export class TLDR { // eslint-disable-next-line @typescript-eslint/no-explicit-any - static getShapeUtils(type: T['type']): TLDrawShapeUtil + static getShapeUtil(type: T['type']): TDShapeUtil // eslint-disable-next-line @typescript-eslint/no-explicit-any - static getShapeUtils(shape: T): TLDrawShapeUtil - static getShapeUtils(shape: T | T['type']) { - return getShapeUtils(shape) + static getShapeUtil(shape: T): TDShapeUtil + static getShapeUtil(shape: T | T['type']) { + return getShapeUtil(shape) } - static getSelectedShapes(data: TLDrawSnapshot, pageId: string) { + static getSelectedShapes(data: TDSnapshot, pageId: string) { const page = TLDR.getPage(data, pageId) const selectedIds = TLDR.getSelectedIds(data, pageId) return selectedIds.map((id) => page.shapes[id]) } - static screenToWorld(data: TLDrawSnapshot, point: number[]) { + static screenToWorld(data: TDSnapshot, point: number[]) { const camera = TLDR.getPageState(data, data.appState.currentPageId).camera return Vec.sub(Vec.div(point, camera.zoom), camera.point) } @@ -39,59 +39,59 @@ export class TLDR { return Utils.clamp(zoom, 0.1, 5) } - static getPage(data: TLDrawSnapshot, pageId: string): TLDrawPage { + static getPage(data: TDSnapshot, pageId: string): TDPage { return data.document.pages[pageId] } - static getPageState(data: TLDrawSnapshot, pageId: string): TLPageState { + static getPageState(data: TDSnapshot, pageId: string): TLPageState { return data.document.pageStates[pageId] } - static getSelectedIds(data: TLDrawSnapshot, pageId: string): string[] { + static getSelectedIds(data: TDSnapshot, pageId: string): string[] { return TLDR.getPageState(data, pageId).selectedIds } - static getShapes(data: TLDrawSnapshot, pageId: string): TLDrawShape[] { + static getShapes(data: TDSnapshot, pageId: string): TDShape[] { return Object.values(TLDR.getPage(data, pageId).shapes) } - static getCamera(data: TLDrawSnapshot, pageId: string): TLPageState['camera'] { + static getCamera(data: TDSnapshot, pageId: string): TLPageState['camera'] { return TLDR.getPageState(data, pageId).camera } - static getShape( - data: TLDrawSnapshot, + static getShape( + data: TDSnapshot, shapeId: string, pageId: string ): T { return TLDR.getPage(data, pageId).shapes[shapeId] as T } - static getCenter(shape: T) { - return TLDR.getShapeUtils(shape).getCenter(shape) + static getCenter(shape: T) { + return TLDR.getShapeUtil(shape).getCenter(shape) } - static getBounds(shape: T) { - return TLDR.getShapeUtils(shape).getBounds(shape) + static getBounds(shape: T) { + return TLDR.getShapeUtil(shape).getBounds(shape) } - static getRotatedBounds(shape: T) { - return TLDR.getShapeUtils(shape).getRotatedBounds(shape) + static getRotatedBounds(shape: T) { + return TLDR.getShapeUtil(shape).getRotatedBounds(shape) } - static getSelectedBounds(data: TLDrawSnapshot): TLBounds { + static getSelectedBounds(data: TDSnapshot): TLBounds { return Utils.getCommonBounds( TLDR.getSelectedShapes(data, data.appState.currentPageId).map((shape) => - TLDR.getShapeUtils(shape).getBounds(shape) + TLDR.getShapeUtil(shape).getBounds(shape) ) ) } - static getParentId(data: TLDrawSnapshot, id: string, pageId: string) { + static getParentId(data: TDSnapshot, id: string, pageId: string) { return TLDR.getShape(data, id, pageId).parentId } - // static getPointedId(data: TLDrawSnapshot, id: string, pageId: string): string { + // static getPointedId(data: TDSnapshot, id: string, pageId: string): string { // const page = TLDR.getPage(data, pageId) // const pageState = TLDR.getPageState(data, data.appState.currentPageId) // const shape = TLDR.getShape(data, id, pageId) @@ -102,7 +102,7 @@ export class TLDR { // : TLDR.getPointedId(data, shape.parentId, pageId) // } - // static getDrilledPointedId(data: TLDrawSnapshot, id: string, pageId: string): string { + // static getDrilledPointedId(data: TDSnapshot, id: string, pageId: string): string { // const shape = TLDR.getShape(data, id, pageId) // const { currentPageId } = data.appState // const { currentParentId, pointedId } = TLDR.getPageState(data, data.appState.currentPageId) @@ -114,7 +114,7 @@ export class TLDR { // : TLDR.getDrilledPointedId(data, shape.parentId, pageId) // } - // static getTopParentId(data: TLDrawSnapshot, id: string, pageId: string): string { + // static getTopParentId(data: TDSnapshot, id: string, pageId: string): string { // const page = TLDR.getPage(data, pageId) // const pageState = TLDR.getPageState(data, pageId) // const shape = TLDR.getShape(data, id, pageId) @@ -129,7 +129,7 @@ export class TLDR { // } // Get an array of a shape id and its descendant shapes' ids - static getDocumentBranch(data: TLDrawSnapshot, id: string, pageId: string): string[] { + static getDocumentBranch(data: TDSnapshot, id: string, pageId: string): string[] { const shape = TLDR.getShape(data, id, pageId) if (shape.children === undefined) return [id] @@ -142,16 +142,16 @@ export class TLDR { // Get a deep array of unproxied shapes and their descendants static getSelectedBranchSnapshot( - data: TLDrawSnapshot, + data: TDSnapshot, pageId: string, - fn: (shape: TLDrawShape) => K + fn: (shape: TDShape) => K ): ({ id: string } & K)[] - static getSelectedBranchSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[] + static getSelectedBranchSnapshot(data: TDSnapshot, pageId: string): TDShape[] static getSelectedBranchSnapshot( - data: TLDrawSnapshot, + data: TDSnapshot, pageId: string, - fn?: (shape: TLDrawShape) => K - ): (TLDrawShape | K)[] { + fn?: (shape: TDShape) => K + ): (TDShape | K)[] { const page = TLDR.getPage(data, pageId) const copies = TLDR.getSelectedIds(data, pageId) @@ -167,17 +167,17 @@ export class TLDR { } // Get a shallow array of unproxied shapes - static getSelectedShapeSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[] + static getSelectedShapeSnapshot(data: TDSnapshot, pageId: string): TDShape[] static getSelectedShapeSnapshot( - data: TLDrawSnapshot, + data: TDSnapshot, pageId: string, - fn?: (shape: TLDrawShape) => K + fn?: (shape: TDShape) => K ): ({ id: string } & K)[] static getSelectedShapeSnapshot( - data: TLDrawSnapshot, + data: TDSnapshot, pageId: string, - fn?: (shape: TLDrawShape) => K - ): (TLDrawShape | K)[] { + fn?: (shape: TDShape) => K + ): (TDShape | K)[] { const copies = TLDR.getSelectedShapes(data, pageId) .filter((shape) => !shape.isLocked) .map(Utils.deepClone) @@ -191,7 +191,7 @@ export class TLDR { // For a given array of shape ids, an array of all other shapes that may be affected by a mutation to it. // Use this to decide which shapes to clone as before / after for a command. - static getAllEffectedShapeIds(data: TLDrawSnapshot, ids: string[], pageId: string): string[] { + static getAllEffectedShapeIds(data: TDSnapshot, ids: string[], pageId: string): string[] { const page = TLDR.getPage(data, pageId) const visited = new Set(ids) @@ -200,7 +200,7 @@ export class TLDR { const shape = page.shapes[id] // Add descendant shapes - function collectDescendants(shape: TLDrawShape): void { + function collectDescendants(shape: TDShape): void { if (shape.children === undefined) return shape.children .filter((childId) => !visited.has(childId)) @@ -213,7 +213,7 @@ export class TLDR { collectDescendants(shape) // Add asecendant shapes - function collectAscendants(shape: TLDrawShape): void { + function collectAscendants(shape: TDShape): void { const parentId = shape.parentId if (parentId === page.id) return if (visited.has(parentId)) return @@ -236,47 +236,47 @@ export class TLDR { } static updateBindings( - data: TLDrawSnapshot, + data: TDSnapshot, id: string, - beforeShapes: Record> = {}, - afterShapes: Record> = {}, + beforeShapes: Record> = {}, + afterShapes: Record> = {}, pageId: string - ): TLDrawSnapshot { + ): TDSnapshot { const page = { ...TLDR.getPage(data, pageId) } return Object.values(page.bindings) .filter((binding) => binding.fromId === id || binding.toId === id) - .reduce((cTLDrawSnapshot, binding) => { + .reduce((cTDSnapshot, binding) => { if (!beforeShapes[binding.fromId]) { beforeShapes[binding.fromId] = Utils.deepClone( - TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId) + TLDR.getShape(cTDSnapshot, binding.fromId, pageId) ) } if (!beforeShapes[binding.toId]) { beforeShapes[binding.toId] = Utils.deepClone( - TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId) + TLDR.getShape(cTDSnapshot, binding.toId, pageId) ) } TLDR.onBindingChange( - TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId), + TLDR.getShape(cTDSnapshot, binding.fromId, pageId), binding, - TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId) + TLDR.getShape(cTDSnapshot, binding.toId, pageId) ) afterShapes[binding.fromId] = Utils.deepClone( - TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId) + TLDR.getShape(cTDSnapshot, binding.fromId, pageId) ) afterShapes[binding.toId] = Utils.deepClone( - TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId) + TLDR.getShape(cTDSnapshot, binding.toId, pageId) ) - return cTLDrawSnapshot + return cTDSnapshot }, data) } - static getLinkedShapes( - data: TLDrawSnapshot, + static getLinkedShapeIds( + data: TDSnapshot, pageId: string, direction: 'center' | 'left' | 'right', includeArrows = true @@ -294,7 +294,7 @@ export class TLDR { const arrows = new Set( Object.values(page.shapes).filter((shape) => { return ( - shape.type === TLDrawShapeType.Arrow && + shape.type === TDShapeType.Arrow && (shape.handles.start.bindingId || shape.handles?.end.bindingId) ) }) as ArrowShape[] @@ -378,11 +378,11 @@ export class TLDR { return Array.from(linkedIds.values()) } - static getChildIndexAbove(data: TLDrawSnapshot, id: string, pageId: string): number { + static getChildIndexAbove(data: TDSnapshot, id: string, pageId: string): number { const page = data.document.pages[pageId] const shape = page.shapes[id] - let siblings: TLDrawShape[] + let siblings: TDShape[] if (shape.parentId === page.id) { siblings = Object.values(page.shapes) @@ -409,27 +409,29 @@ export class TLDR { /* Mutations */ /* -------------------------------------------------- */ - static getBeforeShape(shape: T, change: Partial): Partial { + static getBeforeShape(shape: T, change: Partial): Partial { return Object.fromEntries( Object.keys(change).map((k) => [k, shape[k as keyof T]]) ) as Partial } - static mutateShapes( - data: TLDrawSnapshot, + static mutateShapes( + data: TDSnapshot, ids: string[], fn: (shape: T, i: number) => Partial | void, pageId: string ): { before: Record> after: Record> - data: TLDrawSnapshot + data: TDSnapshot } { const beforeShapes: Record> = {} const afterShapes: Record> = {} ids.forEach((id, i) => { const shape = TLDR.getShape(data, id, pageId) + if (shape.isLocked) return + const change = fn(shape, i) if (change) { beforeShapes[id] = TLDR.getBeforeShape(shape, change) @@ -446,8 +448,8 @@ export class TLDR { }, }, }) - const dataWithBindingChanges = ids.reduce((cTLDrawSnapshot, id) => { - return TLDR.updateBindings(cTLDrawSnapshot, id, beforeShapes, afterShapes, pageId) + const dataWithBindingChanges = ids.reduce((cTDSnapshot, id) => { + return TLDR.updateBindings(cTDSnapshot, id, beforeShapes, afterShapes, pageId) }, dataWithMutations) return { @@ -457,17 +459,15 @@ export class TLDR { } } - static createShapes(data: TLDrawSnapshot, shapes: TLDrawShape[], pageId: string): TLDrawCommand { - const before: TLDrawPatch = { + static createShapes(data: TDSnapshot, shapes: TDShape[], pageId: string): TldrawCommand { + const before: TldrawPatch = { document: { pages: { [pageId]: { shapes: { ...Object.fromEntries( shapes.flatMap((shape) => { - const results: [string, Partial | undefined][] = [ - [shape.id, undefined], - ] + const results: [string, Partial | undefined][] = [[shape.id, undefined]] // If the shape is a child of another shape, also save that shape if (shape.parentId !== pageId) { @@ -485,7 +485,7 @@ export class TLDR { }, } - const after: TLDrawPatch = { + const after: TldrawPatch = { document: { pages: { [pageId]: { @@ -493,9 +493,7 @@ export class TLDR { shapes: { ...Object.fromEntries( shapes.flatMap((shape) => { - const results: [string, Partial | undefined][] = [ - [shape.id, shape], - ] + const results: [string, Partial | undefined][] = [[shape.id, shape]] // If the shape is a child of a different shape, update its parent if (shape.parentId !== pageId) { @@ -521,10 +519,10 @@ export class TLDR { } static deleteShapes( - data: TLDrawSnapshot, - shapes: TLDrawShape[] | string[], + data: TDSnapshot, + shapes: TDShape[] | string[], pageId?: string - ): TLDrawCommand { + ): TldrawCommand { pageId = pageId ? pageId : data.appState.currentPageId const page = TLDR.getPage(data, pageId) @@ -532,9 +530,9 @@ export class TLDR { const shapeIds = typeof shapes[0] === 'string' ? (shapes as string[]) - : (shapes as TLDrawShape[]).map((shape) => shape.id) + : (shapes as TDShape[]).map((shape) => shape.id) - const before: TLDrawPatch = { + const before: TldrawPatch = { document: { pages: { [pageId]: { @@ -543,7 +541,7 @@ export class TLDR { ...Object.fromEntries( shapeIds.flatMap((id) => { const shape = page.shapes[id] - const results: [string, Partial | undefined][] = [[shape.id, shape]] + const results: [string, Partial | undefined][] = [[shape.id, shape]] // If the shape is a child of another shape, also add that shape if (shape.parentId !== pageId) { @@ -573,7 +571,7 @@ export class TLDR { }, } - const after: TLDrawPatch = { + const after: TldrawPatch = { document: { pages: { [pageId]: { @@ -581,9 +579,7 @@ export class TLDR { ...Object.fromEntries( shapeIds.flatMap((id) => { const shape = page.shapes[id] - const results: [string, Partial | undefined][] = [ - [shape.id, undefined], - ] + const results: [string, Partial | undefined][] = [[shape.id, undefined]] // If the shape is a child of a different shape, update its parent if (shape.parentId !== page.id) { @@ -612,16 +608,16 @@ export class TLDR { } } - static onSessionComplete(shape: T) { - const delta = TLDR.getShapeUtils(shape).onSessionComplete?.(shape) + static onSessionComplete(shape: T) { + const delta = TLDR.getShapeUtil(shape).onSessionComplete?.(shape) if (!delta) return shape return { ...shape, ...delta } } - static onChildrenChange(data: TLDrawSnapshot, shape: T, pageId: string) { + static onChildrenChange(data: TDSnapshot, shape: T, pageId: string) { if (!shape.children) return - const delta = TLDR.getShapeUtils(shape).onChildrenChange?.( + const delta = TLDR.getShapeUtil(shape).onChildrenChange?.( shape, shape.children.map((id) => TLDR.getShape(data, id, pageId)) ) @@ -631,35 +627,27 @@ export class TLDR { return { ...shape, ...delta } } - static onBindingChange( - shape: T, - binding: TLDrawBinding, - otherShape: TLDrawShape - ) { - const delta = TLDR.getShapeUtils(shape).onBindingChange?.( + static onBindingChange(shape: T, binding: TDBinding, otherShape: TDShape) { + const delta = TLDR.getShapeUtil(shape).onBindingChange?.( shape, binding, otherShape, - TLDR.getShapeUtils(otherShape).getBounds(otherShape), - TLDR.getShapeUtils(otherShape).getCenter(otherShape) + TLDR.getShapeUtil(otherShape).getBounds(otherShape), + TLDR.getShapeUtil(otherShape).getCenter(otherShape) ) if (!delta) return shape return { ...shape, ...delta } } - static transform(shape: T, bounds: TLBounds, info: TLTransformInfo) { - const delta = TLDR.getShapeUtils(shape).transform(shape, bounds, info) + static transform(shape: T, bounds: TLBounds, info: TLTransformInfo) { + const delta = TLDR.getShapeUtil(shape).transform(shape, bounds, info) if (!delta) return shape return { ...shape, ...delta } } - static transformSingle( - shape: T, - bounds: TLBounds, - info: TLTransformInfo - ) { - const delta = TLDR.getShapeUtils(shape).transformSingle(shape, bounds, info) + static transformSingle(shape: T, bounds: TLBounds, info: TLTransformInfo) { + const delta = TLDR.getShapeUtil(shape).transformSingle(shape, bounds, info) if (!delta) return shape return { ...shape, ...delta } } @@ -671,7 +659,7 @@ export class TLDR { * @param origin the page point to rotate around. * @param rotation the amount to rotate the shape. */ - static getRotatedShapeMutation( + static getRotatedShapeMutation( shape: T, // in page space center: number[], // in page space origin: number[], // in page space (probably the center of common bounds) @@ -690,7 +678,7 @@ export class TLDR { // of rotating the shape. Shapes with handles should never be rotated, // because that makes a lot of other things incredible difficult. if (shape.handles !== undefined) { - const change = this.getShapeUtils(shape).onHandleChange?.( + const change = this.getShapeUtil(shape).onHandleChange?.( // Base the change on a shape with the next point { ...shape, point: nextPoint }, Object.fromEntries( @@ -723,7 +711,7 @@ export class TLDR { /* Parents */ /* -------------------------------------------------- */ - static updateParents(data: TLDrawSnapshot, pageId: string, changedShapeIds: string[]): void { + static updateParents(data: TDSnapshot, pageId: string, changedShapeIds: string[]): void { const page = TLDR.getPage(data, pageId) if (changedShapeIds.length === 0) return @@ -747,7 +735,7 @@ export class TLDR { TLDR.updateParents(data, pageId, parentToUpdateIds) } - static getSelectedStyle(data: TLDrawSnapshot, pageId: string): ShapeStyles | false { + static getSelectedStyle(data: TDSnapshot, pageId: string): ShapeStyles | false { const { currentStyle } = data.appState const page = data.document.pages[pageId] @@ -788,27 +776,23 @@ export class TLDR { /* Bindings */ /* -------------------------------------------------- */ - static getBinding(data: TLDrawSnapshot, id: string, pageId: string): TLDrawBinding { + static getBinding(data: TDSnapshot, id: string, pageId: string): TDBinding { return TLDR.getPage(data, pageId).bindings[id] } - static getBindings(data: TLDrawSnapshot, pageId: string): TLDrawBinding[] { + static getBindings(data: TDSnapshot, pageId: string): TDBinding[] { const page = TLDR.getPage(data, pageId) return Object.values(page.bindings) } - static getBindableShapeIds(data: TLDrawSnapshot) { + static getBindableShapeIds(data: TDSnapshot) { return TLDR.getShapes(data, data.appState.currentPageId) - .filter((shape) => TLDR.getShapeUtils(shape).canBind) + .filter((shape) => TLDR.getShapeUtil(shape).canBind) .sort((a, b) => b.childIndex - a.childIndex) .map((shape) => shape.id) } - static getBindingsWithShapeIds( - data: TLDrawSnapshot, - ids: string[], - pageId: string - ): TLDrawBinding[] { + static getBindingsWithShapeIds(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] { return Array.from( new Set( TLDR.getBindings(data, pageId).filter((binding) => { @@ -818,7 +802,7 @@ export class TLDR { ) } - static getRelatedBindings(data: TLDrawSnapshot, ids: string[], pageId: string): TLDrawBinding[] { + static getRelatedBindings(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] { const changedShapeIds = new Set(ids) const page = TLDR.getPage(data, pageId) @@ -897,7 +881,7 @@ export class TLDR { /* Groups */ /* -------------------------------------------------- */ - static flattenShape = (data: TLDrawSnapshot, shape: TLDrawShape): TLDrawShape[] => { + static flattenShape = (data: TDSnapshot, shape: TDShape): TDShape[] => { return [ shape, ...(shape.children ?? []) @@ -907,13 +891,13 @@ export class TLDR { ] } - static flattenPage = (data: TLDrawSnapshot, pageId: string): TLDrawShape[] => { + static flattenPage = (data: TDSnapshot, pageId: string): TDShape[] => { return Object.values(data.document.pages[pageId].shapes) .sort((a, b) => a.childIndex - b.childIndex) - .reduce((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], []) + .reduce((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], []) } - static getTopChildIndex = (data: TLDrawSnapshot, pageId: string): number => { + static getTopChildIndex = (data: TDSnapshot, pageId: string): number => { const shapes = TLDR.getShapes(data, pageId) return shapes.length === 0 ? 1 @@ -922,12 +906,23 @@ export class TLDR { .sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1 } + /* -------------------------------------------------- */ + /* Text */ + /* -------------------------------------------------- */ + + static fixNewLines = /\r?\n|\r/g + static fixSpaces = / /g + + static normalizeText(text: string) { + return text.replace(TLDR.fixNewLines, '\n').replace(TLDR.fixSpaces, '\u00a0') + } + /* -------------------------------------------------- */ /* Assertions */ /* -------------------------------------------------- */ - static assertShapeHasProperty

    ( - shape: TLDrawShape, + static assertShapeHasProperty

    ( + shape: TDShape, prop: P ): asserts shape is ShapesWithProp

    { if (shape[prop] === undefined) { diff --git a/packages/tldraw/src/state/TLDrawState.spec.ts b/packages/tldraw/src/state/TLDrawState.spec.ts deleted file mode 100644 index d277cb5e2..000000000 --- a/packages/tldraw/src/state/TLDrawState.spec.ts +++ /dev/null @@ -1,653 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { TLDrawState } from './TLDrawState' -import { mockDocument, TLDrawStateUtils } from '~test' -import { ArrowShape, ColorStyle, SessionType, TLDrawShapeType } from '~types' -import type { SelectTool } from './tools/SelectTool' - -describe('TLDrawState', () => { - const state = new TLDrawState() - - const tlu = new TLDrawStateUtils(state) - - describe('When copying and pasting...', () => { - it('copies a shape', () => { - state.loadDocument(mockDocument).selectNone().copy(['rect1']) - }) - - it('pastes a shape', () => { - state.loadDocument(mockDocument) - - const prevCount = Object.keys(state.page.shapes).length - - state.selectNone().copy(['rect1']).paste() - - expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1) - - state.undo() - - expect(Object.keys(state.page.shapes).length).toBe(prevCount) - - state.redo() - - expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1) - }) - - it('pastes a shape to a new page', () => { - state.loadDocument(mockDocument) - - state.selectNone().copy(['rect1']).createPage().paste() - - expect(Object.keys(state.page.shapes).length).toBe(1) - - state.undo() - - expect(Object.keys(state.page.shapes).length).toBe(0) - - state.redo() - - expect(Object.keys(state.page.shapes).length).toBe(1) - }) - - it('Copies grouped shapes.', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .group(['rect1', 'rect2'], 'groupA') - .select('groupA') - .copy() - - const beforeShapes = state.shapes - - state.paste() - - expect(state.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(2) - - const afterShapes = state.shapes - - const newShapes = afterShapes.filter( - (shape) => !beforeShapes.find(({ id }) => id === shape.id) - ) - - const newGroup = newShapes.find((shape) => shape.type === TLDrawShapeType.Group) - - const newChildIds = newShapes - .filter((shape) => shape.type !== TLDrawShapeType.Group) - .map((shape) => shape.id) - - expect(new Set(newGroup!.children)).toEqual(new Set(newChildIds)) - }) - - it.todo("Pastes in to the top child index of the page's children.") - - it.todo('Pastes in the correct child index order.') - }) - - describe('When copying and pasting a shape with bindings', () => { - it('copies two bound shapes and their binding', () => { - const state = new TLDrawState() - - state - .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } - ) - .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([55, 55]) - .completeSession() - - expect(state.bindings.length).toBe(1) - - state.selectAll().copy().paste() - - const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape - - expect(newArrow.handles.start.bindingId).not.toBe( - state.getShape('arrow1').handles.start.bindingId - ) - - expect(state.bindings.length).toBe(2) - }) - - it('removes bindings from copied shape handles', () => { - const state = new TLDrawState() - - state - .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } - ) - .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([55, 55]) - .completeSession() - - expect(state.bindings.length).toBe(1) - - expect(state.getShape('arrow1').handles.start.bindingId).toBeDefined() - - state.select('arrow1').copy().paste() - - const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape - - expect(newArrow.handles.start.bindingId).toBeUndefined() - }) - }) - - describe('Selection', () => { - it('selects a shape', () => { - state.loadDocument(mockDocument).selectNone() - tlu.clickShape('rect1') - expect(state.selectedIds).toStrictEqual(['rect1']) - expect(state.appState.status).toBe('idle') - }) - - it('selects and deselects a shape', () => { - state.loadDocument(mockDocument).selectNone() - tlu.clickShape('rect1') - tlu.clickCanvas() - expect(state.selectedIds).toStrictEqual([]) - expect(state.appState.status).toBe('idle') - }) - - it('selects multiple shapes', () => { - state.loadDocument(mockDocument).selectNone() - tlu.clickShape('rect1') - tlu.clickShape('rect2', { shiftKey: true }) - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) - expect(state.appState.status).toBe('idle') - }) - - it('shift-selects to deselect shapes', () => { - state.loadDocument(mockDocument).selectNone() - tlu.clickShape('rect1') - tlu.clickShape('rect2', { shiftKey: true }) - tlu.clickShape('rect2', { shiftKey: true }) - expect(state.selectedIds).toStrictEqual(['rect1']) - expect(state.appState.status).toBe('idle') - }) - - it('clears selection when clicking bounds', () => { - state.loadDocument(mockDocument).selectNone() - state.startSession(SessionType.Brush, [-10, -10]) - state.updateSession([110, 110]) - state.completeSession() - expect(state.selectedIds.length).toBe(3) - }) - - it('selects selected shape when single-clicked', () => { - state.loadDocument(mockDocument).selectAll() - tlu.clickShape('rect2') - expect(state.selectedIds).toStrictEqual(['rect2']) - }) - - // it('selects shape when double-clicked', () => { - // state.loadDocument(mockDocument).selectAll() - // tlu.doubleClickShape('rect2') - // expect(state.selectedIds).toStrictEqual(['rect2']) - // }) - - it('does not select on meta-click', () => { - state.loadDocument(mockDocument).selectNone() - tlu.clickShape('rect1', { ctrlKey: true }) - expect(state.selectedIds).toStrictEqual([]) - expect(state.appState.status).toBe('idle') - }) - - it.todo('deletes shapes if cancelled during creating') - - it.todo('deletes shapes on undo after creating') - - it.todo('re-creates shapes on redo after creating') - - describe('When selecting all', () => { - it('selects all', () => { - const state = new TLDrawState().loadDocument(mockDocument).selectAll() - expect(state.selectedIds).toMatchSnapshot('selected all') - }) - - it('does not select children of a group', () => { - const state = new TLDrawState().loadDocument(mockDocument).selectAll().group() - expect(state.selectedIds.length).toBe(1) - }) - }) - - // Single click on a selected shape to select just that shape - - it('single-selects shape in selection on click', () => { - state.selectNone() - tlu.clickShape('rect1') - tlu.clickShape('rect2', { shiftKey: true }) - tlu.clickShape('rect2') - expect(state.selectedIds).toStrictEqual(['rect2']) - expect(state.appState.status).toBe('idle') - }) - - it('single-selects shape in selection on pointerup only', () => { - state.selectNone() - tlu.clickShape('rect1') - tlu.clickShape('rect2', { shiftKey: true }) - tlu.pointShape('rect2') - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) - tlu.stopPointing('rect2') - expect(state.selectedIds).toStrictEqual(['rect2']) - expect(state.appState.status).toBe('idle') - }) - - // it('selects shapes if shift key is lifted before pointerup', () => { - // state.selectNone() - // tlu.clickShape('rect1') - // tlu.pointShape('rect2', { shiftKey: true }) - // expect(state.appState.status).toBe('pointingBounds') - // tlu.stopPointing('rect2') - // expect(state.selectedIds).toStrictEqual(['rect2']) - // expect(state.appState.status).toBe('idle') - // }) - }) - - describe('Select history', () => { - it('selects, undoes and redoes', () => { - state.reset().loadDocument(mockDocument) - - expect(state.selectHistory.pointer).toBe(0) - expect(state.selectHistory.stack).toStrictEqual([[]]) - expect(state.selectedIds).toStrictEqual([]) - - tlu.pointShape('rect1') - - expect(state.selectHistory.pointer).toBe(1) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']]) - expect(state.selectedIds).toStrictEqual(['rect1']) - - tlu.stopPointing('rect1') - - expect(state.selectHistory.pointer).toBe(1) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']]) - expect(state.selectedIds).toStrictEqual(['rect1']) - - tlu.clickShape('rect2', { shiftKey: true }) - - expect(state.selectHistory.pointer).toBe(2) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) - - state.undoSelect() - - expect(state.selectHistory.pointer).toBe(1) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) - expect(state.selectedIds).toStrictEqual(['rect1']) - - state.undoSelect() - - expect(state.selectHistory.pointer).toBe(0) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) - expect(state.selectedIds).toStrictEqual([]) - - state.redoSelect() - - expect(state.selectHistory.pointer).toBe(1) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) - expect(state.selectedIds).toStrictEqual(['rect1']) - - state.select('rect2') - - expect(state.selectHistory.pointer).toBe(2) - expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']]) - expect(state.selectedIds).toStrictEqual(['rect2']) - - state.delete() - - expect(state.selectHistory.pointer).toBe(0) - expect(state.selectHistory.stack).toStrictEqual([[]]) - expect(state.selectedIds).toStrictEqual([]) - - state.undoSelect() - - expect(state.selectHistory.pointer).toBe(0) - expect(state.selectHistory.stack).toStrictEqual([[]]) - expect(state.selectedIds).toStrictEqual([]) - }) - }) - - describe('Copies to JSON', () => { - state.selectAll() - expect(state.copyJson()).toMatchSnapshot('copied json') - }) - - describe('Mutates bound shapes', () => { - const state = new TLDrawState() - .createShapes( - { - id: 'rect', - point: [0, 0], - size: [100, 100], - childIndex: 1, - type: TLDrawShapeType.Rectangle, - }, - { - id: 'arrow', - point: [200, 200], - childIndex: 2, - type: TLDrawShapeType.Arrow, - } - ) - .select('arrow') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([10, 10]) - .completeSession() - .selectAll() - .style({ color: ColorStyle.Red }) - - expect(state.getShape('arrow').style.color).toBe(ColorStyle.Red) - expect(state.getShape('rect').style.color).toBe(ColorStyle.Red) - - state.undo() - - expect(state.getShape('arrow').style.color).toBe(ColorStyle.Black) - expect(state.getShape('rect').style.color).toBe(ColorStyle.Black) - - state.redo() - - expect(state.getShape('arrow').style.color).toBe(ColorStyle.Red) - expect(state.getShape('rect').style.color).toBe(ColorStyle.Red) - }) - - describe('when selecting shapes in a group', () => { - it('selects the group when a grouped shape is clicked', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') - - const tlu = new TLDrawStateUtils(state) - tlu.clickShape('rect1') - expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined() - expect(state.selectedIds).toStrictEqual(['groupA']) - }) - - it('selects the grouped shape when double clicked', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') - - const tlu = new TLDrawStateUtils(state) - tlu.doubleClickShape('rect1') - expect((state.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA') - expect(state.selectedIds).toStrictEqual(['rect1']) - }) - - it('clears the selectedGroupId when selecting a different shape', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') - - const tlu = new TLDrawStateUtils(state) - tlu.doubleClickShape('rect1') - tlu.clickShape('rect3') - expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined() - expect(state.selectedIds).toStrictEqual(['rect3']) - }) - - it('selects a grouped shape when meta-shift-clicked', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .group(['rect1', 'rect2'], 'groupA') - .selectNone() - - const tlu = new TLDrawStateUtils(state) - - tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true }) - expect(state.selectedIds).toStrictEqual(['rect1']) - - tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true }) - expect(state.selectedIds).toStrictEqual([]) - }) - - it('selects a hovered shape from the selected group when meta-shift-clicked', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') - - const tlu = new TLDrawStateUtils(state) - - tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true }) - expect(state.selectedIds).toStrictEqual(['rect1']) - - tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true }) - expect(state.selectedIds).toStrictEqual([]) - }) - }) - - describe('when creating shapes', () => { - it('Creates shapes with the correct child index', () => { - const state = new TLDrawState() - .createShapes( - { - id: 'rect1', - type: TLDrawShapeType.Rectangle, - childIndex: 1, - }, - { - id: 'rect2', - type: TLDrawShapeType.Rectangle, - childIndex: 2, - }, - { - id: 'rect3', - type: TLDrawShapeType.Rectangle, - childIndex: 3, - } - ) - .selectTool(TLDrawShapeType.Rectangle) - - const tlu = new TLDrawStateUtils(state) - - const prevA = state.shapes.map((shape) => shape.id) - - tlu.pointCanvas({ x: 0, y: 0 }) - tlu.movePointer({ x: 100, y: 100 }) - tlu.stopPointing() - - const newIdA = state.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))! - const shapeA = state.getShape(newIdA) - expect(shapeA.childIndex).toBe(4) - - state.group(['rect2', 'rect3', newIdA], 'groupA') - - expect(state.getShape('groupA').childIndex).toBe(2) - - state.selectNone() - state.selectTool(TLDrawShapeType.Rectangle) - - const prevB = state.shapes.map((shape) => shape.id) - - tlu.pointCanvas({ x: 0, y: 0 }) - tlu.movePointer({ x: 100, y: 100 }) - tlu.stopPointing() - - const newIdB = state.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))! - const shapeB = state.getShape(newIdB) - expect(shapeB.childIndex).toBe(3) - }) - }) - - it('Exposes undo/redo stack', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .createShapes({ - id: 'rect1', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - .createShapes({ - id: 'rect2', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - - expect(state.history.length).toBe(2) - - expect(state.history).toBeDefined() - expect(state.history).toMatchSnapshot('history') - - state.history = [] - expect(state.history).toEqual([]) - - const before = state.state - state.undo() - const after = state.state - - expect(before).toBe(after) - }) - - it('Exposes undo/redo stack up to the current pointer', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .createShapes({ - id: 'rect1', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - .createShapes({ - id: 'rect2', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - .undo() - - expect(state.history.length).toBe(1) - }) - - it('Sets the undo/redo history', () => { - const state = new TLDrawState('some_state_a') - .createShapes({ - id: 'rect1', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - .createShapes({ - id: 'rect2', - type: TLDrawShapeType.Rectangle, - point: [0, 0], - size: [100, 200], - }) - - // Save the history and document from the first state - const doc = state.document - const history = state.history - - // Create a new state - const state2 = new TLDrawState('some_state_b') - - // Load the document and set the history - state2.loadDocument(doc) - state2.history = history - - expect(state2.shapes.length).toBe(2) - - // We should be able to undo the change that was made on the first - // state, now that we've brought in its undo / redo stack - state2.undo() - - expect(state2.shapes.length).toBe(1) - }) - - describe('When copying to SVG', () => { - it('Copies shapes.', () => { - const state = new TLDrawState() - const result = state - .loadDocument(mockDocument) - .select('rect1') - .rotate(0.1) - .selectAll() - .copySvg() - expect(result).toMatchSnapshot('copied svg') - }) - - it('Copies grouped shapes.', () => { - const state = new TLDrawState() - const result = state - .loadDocument(mockDocument) - .select('rect1', 'rect2') - .group() - .selectAll() - .copySvg() - - expect(result).toMatchSnapshot('copied svg with group') - }) - - it.todo('Copies Text shapes as elements.') - // it('Copies Text shapes as elements.', () => { - // const state2 = new TLDrawState() - - // const svgString = state2 - // .createShapes({ - // id: 'text1', - // type: TLDrawShapeType.Text, - // text: 'hello world!', - // }) - // .select('text1') - // .copySvg() - - // expect(svgString).toBeTruthy() - // }) - }) - - describe('when the document prop changes', () => { - it.todo('replaces the document if the ids are different') - - it.todo('updates the document if the new id is the same as the old one') - }) - /* - We want to be able to use the `document` property to update the - document without blowing out the current state. For example, we - may want to patch in changes that occurred from another user. - - When the `document` prop changes in the TLDraw component, we want - to update the document in a way that preserves the identity of as - much as possible, while still protecting against invalid states. - - If this isn't possible, then we should guide the developer to - instead use a helper like `patchDocument` to update the document. - - If the `id` property of the new document is the same as the - previous document, then we call `updateDocument`. Otherwise, we - call `replaceDocument`, which does a harder reset of the state's - internal state. - */ - - jest.setTimeout(10000) - - describe('When changing versions', () => { - it('migrates correctly', (done) => { - const defaultState = TLDrawState.defaultState - - const withoutRoom = { - ...defaultState, - } - - delete withoutRoom.room - - TLDrawState.defaultState = withoutRoom - - const state = new TLDrawState('migrate_1') - - state.createShapes({ - id: 'rect1', - type: TLDrawShapeType.Rectangle, - }) - - setTimeout(() => { - // TODO: Force the version to change and restore room. - TLDrawState.version = 100 - TLDrawState.defaultState.room = defaultState.room - - const state2 = new TLDrawState('migrate_1') - - setTimeout(() => { - try { - expect(state2.getShape('rect1')).toBeTruthy() - done() - } catch (e) { - done(e) - } - }, 100) - }, 100) - }) - }) -}) diff --git a/packages/tldraw/src/state/TldrawApp.spec.ts b/packages/tldraw/src/state/TldrawApp.spec.ts new file mode 100644 index 000000000..6182985a3 --- /dev/null +++ b/packages/tldraw/src/state/TldrawApp.spec.ts @@ -0,0 +1,661 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, ColorStyle, SessionType, TDShapeType } from '~types' +import type { SelectTool } from './tools/SelectTool' + +describe('TldrawTestApp', () => { + describe('When copying and pasting...', () => { + it('copies a shape', () => { + const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1']) + }) + + it('pastes a shape', () => { + const app = new TldrawTestApp().loadDocument(mockDocument) + + const prevCount = Object.keys(app.page.shapes).length + + app.selectNone().copy(['rect1']).paste() + + expect(Object.keys(app.page.shapes).length).toBe(prevCount + 1) + + app.undo() + + expect(Object.keys(app.page.shapes).length).toBe(prevCount) + + app.redo() + + expect(Object.keys(app.page.shapes).length).toBe(prevCount + 1) + }) + + it('pastes a shape to a new page', () => { + const app = new TldrawTestApp().loadDocument(mockDocument) + + app.selectNone().copy(['rect1']).createPage().paste() + + expect(Object.keys(app.page.shapes).length).toBe(1) + + app.undo() + + expect(Object.keys(app.page.shapes).length).toBe(0) + + app.redo() + + expect(Object.keys(app.page.shapes).length).toBe(1) + }) + + it('Copies grouped shapes.', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .select('groupA') + .copy() + + const beforeShapes = app.shapes + + app.paste() + + expect(app.shapes.filter((shape) => shape.type === TDShapeType.Group).length).toBe(2) + + const afterShapes = app.shapes + + const newShapes = afterShapes.filter( + (shape) => !beforeShapes.find(({ id }) => id === shape.id) + ) + + const newGroup = newShapes.find((shape) => shape.type === TDShapeType.Group) + + const newChildIds = newShapes + .filter((shape) => shape.type !== TDShapeType.Group) + .map((shape) => shape.id) + + expect(new Set(newGroup!.children)).toEqual(new Set(newChildIds)) + }) + + it.todo("Pastes in to the top child index of the page's children.") + + it.todo('Pastes in the correct child index order.') + }) + + describe('When copying and pasting a shape with bindings', () => { + it('copies two bound shapes and their binding', () => { + const app = new TldrawTestApp() + + app + .createShapes( + { type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } + ) + .select('arrow1') + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([55, 55]) + .completeSession() + + expect(app.bindings.length).toBe(1) + + app.selectAll().copy().paste() + + const newArrow = app.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape + + expect(newArrow.handles.start.bindingId).not.toBe( + app.getShape('arrow1').handles.start.bindingId + ) + + expect(app.bindings.length).toBe(2) + }) + + it('removes bindings from copied shape handles', () => { + const app = new TldrawTestApp() + + app + .createShapes( + { type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } + ) + .select('arrow1') + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([55, 55]) + .completeSession() + + expect(app.bindings.length).toBe(1) + + expect(app.getShape('arrow1').handles.start.bindingId).toBeDefined() + + app.select('arrow1').copy().paste() + + const newArrow = app.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape + + expect(newArrow.handles.start.bindingId).toBeUndefined() + }) + }) + + describe('Selection', () => { + it('selects a shape', () => { + const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().clickShape('rect1') + expect(app.selectedIds).toStrictEqual(['rect1']) + expect(app.status).toBe('idle') + }) + + it('selects and deselects a shape', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectNone() + .clickShape('rect1') + .clickCanvas() + expect(app.selectedIds).toStrictEqual([]) + expect(app.status).toBe('idle') + }) + + it('selects multiple shapes', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectNone() + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) + expect(app.status).toBe('idle') + }) + + it('shift-selects to deselect shapes', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectNone() + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .clickShape('rect2', { shiftKey: true }) + expect(app.selectedIds).toStrictEqual(['rect1']) + expect(app.status).toBe('idle') + }) + + it('clears selection when clicking bounds', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectAll() + .clickBounds() + .completeSession() + expect(app.selectedIds.length).toBe(0) + }) + + it('selects selected shape when single-clicked', () => { + new TldrawTestApp() + .loadDocument(mockDocument) + .selectAll() + .expectSelectedIdsToBe(['rect1', 'rect2', 'rect3']) + .pointShape('rect1') + .pointBounds() // because it is selected, argh + .stopPointing('rect1') + .expectSelectedIdsToBe(['rect1']) + }) + + // it('selects shape when double-clicked', () => { + // app.loadDocument(mockDocument).selectAll() + // .doubleClickShape('rect2') + // expect(app.selectedIds).toStrictEqual(['rect2']) + // }) + + it('does not select on meta-click', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectNone() + .clickShape('rect1', { ctrlKey: true }) + .expectSelectedIdsToBe([]) + + expect(app.status).toBe('idle') + }) + + it.todo('deletes shapes if cancelled during creating') + + it.todo('deletes shapes on undo after creating') + + it.todo('re-creates shapes on redo after creating') + + describe('When selecting all', () => { + it('selects all', () => { + const app = new TldrawTestApp().loadDocument(mockDocument).selectAll() + expect(app.selectedIds).toMatchSnapshot('selected all') + }) + + it('does not select children of a group', () => { + const app = new TldrawTestApp().loadDocument(mockDocument).selectAll().group() + expect(app.selectedIds.length).toBe(1) + }) + }) + + // Single click on a selected shape to select just that shape + + it('single-selects shape in selection on click', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .clickShape('rect2') + expect(app.selectedIds).toStrictEqual(['rect2']) + expect(app.status).toBe('idle') + }) + + it('single-selects shape in selection on pointerup only', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .clickShape('rect1') + .clickShape('rect2', { shiftKey: true }) + .pointShape('rect2') + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) + app.stopPointing('rect2') + expect(app.selectedIds).toStrictEqual(['rect2']) + expect(app.status).toBe('idle') + }) + + // it('selects shapes if shift key is lifted before pointerup', () => { + // app.selectNone() + // .clickShape('rect1') + // .pointShape('rect2', { shiftKey: true }) + // expect(app.status).toBe('pointingBounds') + // .stopPointing('rect2') + // expect(app.selectedIds).toStrictEqual(['rect2']) + // expect(app.status).toBe('idle') + // }) + }) + + describe('Select history', () => { + it('selects, undoes and redoes', () => { + const app = new TldrawTestApp().loadDocument(mockDocument) + + expect(app.selectHistory.pointer).toBe(0) + expect(app.selectHistory.stack).toStrictEqual([[]]) + expect(app.selectedIds).toStrictEqual([]) + app.pointShape('rect1') + + expect(app.selectHistory.pointer).toBe(1) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1']]) + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.stopPointing('rect1') + + expect(app.selectHistory.pointer).toBe(1) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1']]) + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.clickShape('rect2', { shiftKey: true }) + + expect(app.selectHistory.pointer).toBe(2) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) + + app.undoSelect() + + expect(app.selectHistory.pointer).toBe(1) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.undoSelect() + + expect(app.selectHistory.pointer).toBe(0) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) + expect(app.selectedIds).toStrictEqual([]) + + app.redoSelect() + + expect(app.selectHistory.pointer).toBe(1) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']]) + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.select('rect2') + + expect(app.selectHistory.pointer).toBe(2) + expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']]) + expect(app.selectedIds).toStrictEqual(['rect2']) + + app.delete() + + expect(app.selectHistory.pointer).toBe(0) + expect(app.selectHistory.stack).toStrictEqual([[]]) + expect(app.selectedIds).toStrictEqual([]) + + app.undoSelect() + + expect(app.selectHistory.pointer).toBe(0) + expect(app.selectHistory.stack).toStrictEqual([[]]) + expect(app.selectedIds).toStrictEqual([]) + }) + }) + + describe('Copies to JSON', () => { + const app = new TldrawTestApp().loadDocument(mockDocument).selectAll() + expect(app.copyJson()).toMatchSnapshot('copied json') + }) + + describe('Mutates bound shapes', () => { + const app = new TldrawTestApp() + .createShapes( + { + id: 'rect', + point: [0, 0], + size: [100, 100], + childIndex: 1, + type: TDShapeType.Rectangle, + }, + { + id: 'arrow', + point: [200, 200], + childIndex: 2, + type: TDShapeType.Arrow, + } + ) + .select('arrow') + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow', 'start') + .movePointer([10, 10]) + .completeSession() + .selectAll() + .style({ color: ColorStyle.Red }) + + expect(app.getShape('arrow').style.color).toBe(ColorStyle.Red) + expect(app.getShape('rect').style.color).toBe(ColorStyle.Red) + + app.undo() + + expect(app.getShape('arrow').style.color).toBe(ColorStyle.Black) + expect(app.getShape('rect').style.color).toBe(ColorStyle.Black) + + app.redo() + + expect(app.getShape('arrow').style.color).toBe(ColorStyle.Red) + expect(app.getShape('rect').style.color).toBe(ColorStyle.Red) + }) + + describe('when selecting shapes in a group', () => { + it('selects the group when a grouped shape is clicked', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .clickShape('rect1') + + expect((app.currentTool as SelectTool).selectedGroupId).toBeUndefined() + expect(app.selectedIds).toStrictEqual(['groupA']) + }) + + it('selects the grouped shape when double clicked', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .doubleClickShape('rect1') + + expect((app.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA') + expect(app.selectedIds).toStrictEqual(['rect1']) + }) + + it('clears the selectedGroupId when selecting a different shape', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .doubleClickShape('rect1') + .clickShape('rect3') + + expect((app.currentTool as SelectTool).selectedGroupId).toBeUndefined() + expect(app.selectedIds).toStrictEqual(['rect3']) + }) + + it('selects a grouped shape when meta-shift-clicked', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .selectNone() + .clickShape('rect1', { ctrlKey: true, shiftKey: true }) + + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.clickShape('rect1', { ctrlKey: true, shiftKey: true }) + + expect(app.selectedIds).toStrictEqual([]) + }) + + it('selects a hovered shape from the selected group when meta-shift-clicked', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .clickShape('rect1', { ctrlKey: true, shiftKey: true }) + + expect(app.selectedIds).toStrictEqual(['rect1']) + + app.clickShape('rect1', { ctrlKey: true, shiftKey: true }) + + expect(app.selectedIds).toStrictEqual([]) + }) + }) + + describe('when creating shapes', () => { + it('Creates shapes with the correct child index', () => { + const app = new TldrawTestApp() + .createShapes( + { + id: 'rect1', + type: TDShapeType.Rectangle, + childIndex: 1, + }, + { + id: 'rect2', + type: TDShapeType.Rectangle, + childIndex: 2, + }, + { + id: 'rect3', + type: TDShapeType.Rectangle, + childIndex: 3, + } + ) + .selectTool(TDShapeType.Rectangle) + + const prevA = app.shapes.map((shape) => shape.id) + + app.pointCanvas({ x: 0, y: 0 }).movePointer({ x: 100, y: 100 }).stopPointing() + + const newIdA = app.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))! + const shapeA = app.getShape(newIdA) + expect(shapeA.childIndex).toBe(4) + + app.group(['rect2', 'rect3', newIdA], 'groupA') + + expect(app.getShape('groupA').childIndex).toBe(2) + + app.selectNone() + app.selectTool(TDShapeType.Rectangle) + + const prevB = app.shapes.map((shape) => shape.id) + + app.pointCanvas({ x: 0, y: 0 }).movePointer({ x: 100, y: 100 }).stopPointing() + + const newIdB = app.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))! + const shapeB = app.getShape(newIdB) + expect(shapeB.childIndex).toBe(3) + }) + }) + + it('Exposes undo/redo stack', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .createShapes({ + id: 'rect1', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + .createShapes({ + id: 'rect2', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + + expect(app.history.length).toBe(2) + + expect(app.history).toBeDefined() + expect(app.history).toMatchSnapshot('history') + + app.history = [] + expect(app.history).toEqual([]) + + const before = app.state + app.undo() + const after = app.state + + expect(before).toBe(after) + }) + + it('Exposes undo/redo stack up to the current pointer', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .createShapes({ + id: 'rect1', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + .createShapes({ + id: 'rect2', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + .undo() + + expect(app.history.length).toBe(1) + }) + + it('Sets the undo/redo history', () => { + const app = new TldrawTestApp('some_state_a') + .createShapes({ + id: 'rect1', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + .createShapes({ + id: 'rect2', + type: TDShapeType.Rectangle, + point: [0, 0], + size: [100, 200], + }) + + // Save the history and document from the first state + const doc = app.document + const history = app.history + + // Create a new state + const state2 = new TldrawTestApp('some_state_b') + + // Load the document and set the history + state2.loadDocument(doc) + state2.history = history + + expect(state2.shapes.length).toBe(2) + + // We should be able to undo the change that was made on the first + // state, now that we've brought in its undo / redo stack + state2.undo() + + expect(state2.shapes.length).toBe(1) + }) + + describe('When copying to SVG', () => { + it('Copies shapes.', () => { + const result = new TldrawTestApp() + .loadDocument(mockDocument) + .select('rect1') + .rotate(0.1) + .selectAll() + .copySvg() + expect(result).toMatchSnapshot('copied svg') + }) + + it('Copies grouped shapes.', () => { + const result = new TldrawTestApp() + .loadDocument(mockDocument) + .select('rect1', 'rect2') + .group() + .selectAll() + .copySvg() + + expect(result).toMatchSnapshot('copied svg with group') + }) + + it.todo('Copies Text shapes as elements.') + // it('Copies Text shapes as elements.', () => { + // const state2 = new TldrawTestApp() + + // const svgString = state2 + // .createShapes({ + // id: 'text1', + // type: TDShapeType.Text, + // text: 'hello world!', + // }) + // .select('text1') + // .copySvg() + + // expect(svgString).toBeTruthy() + // }) + }) + + describe('when the document prop changes', () => { + it.todo('replaces the document if the ids are different') + + it.todo('updates the document if the new id is the same as the old one') + }) + /* + We want to be able to use the `document` property to update the + document without blowing out the current app. For example, we + may want to patch in changes that occurred from another user. + + When the `document` prop changes in the Tldraw component, we want + to update the document in a way that preserves the identity of as + much as possible, while still protecting against invalid states. + + If this isn't possible, then we should guide the developer to + instead use a helper like `patchDocument` to update the document. + + If the `id` property of the new document is the same as the + previous document, then we call `updateDocument`. Otherwise, we + call `replaceDocument`, which does a harder reset of the state's + internal app. + */ + + jest.setTimeout(10000) + + describe('When changing versions', () => { + it('migrates correctly', (done) => { + const defaultState = TldrawTestApp.defaultState + + const withoutRoom = { + ...defaultState, + } + + delete withoutRoom.room + + TldrawTestApp.defaultState = withoutRoom + + const app = new TldrawTestApp('migrate_1') + + app.createShapes({ + id: 'rect1', + type: TDShapeType.Rectangle, + }) + + setTimeout(() => { + // TODO: Force the version to change and restore room. + TldrawTestApp.version = 100 + TldrawTestApp.defaultState.room = defaultState.room + + const app2 = new TldrawTestApp('migrate_1') + + setTimeout(() => { + try { + expect(app2.getShape('rect1')).toBeTruthy() + done() + } catch (e) { + done(e) + } + }, 100) + }, 100) + }) + }) +}) diff --git a/packages/tldraw/src/state/TLDrawState.ts b/packages/tldraw/src/state/TldrawApp.ts similarity index 75% rename from packages/tldraw/src/state/TLDrawState.ts rename to packages/tldraw/src/state/TldrawApp.ts index fed648da5..3e6a738bd 100644 --- a/packages/tldraw/src/state/TLDrawState.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -18,27 +18,23 @@ import { } from '@tldraw/core' import { FlipType, - TLDrawDocument, + TDDocument, MoveType, AlignType, StretchType, DistributeType, ShapeStyles, - TLDrawShape, - TLDrawShapeType, - TLDrawSnapshot, - Session, - TLDrawStatus, - SelectHistory, - TLDrawPage, - TLDrawBinding, + TDShape, + TDShapeType, + TDSnapshot, + TDStatus, + TDPage, + TDBinding, GroupShape, - TLDrawCommand, - TLDrawUser, + TldrawCommand, + TDUser, SessionType, - ExceptFirst, - ExceptFirstTwo, - TLDrawToolType, + TDToolType, } from '~types' import { migrate, @@ -49,125 +45,162 @@ import { } from './data' import { TLDR } from './TLDR' import { shapeUtils } from '~state/shapes' -import { defaultStyle } from '~state/shapes/shape-styles' +import { defaultStyle } from '~state/shapes/shared/shape-styles' import * as Commands from './commands' -import { ArgsOfType, getSession } from './sessions' -import { createTools } from './tools' +import { SessionArgsOfType, getSession, TldrawSession } from './sessions' import type { BaseTool } from './tools/BaseTool' import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants' +import { SelectTool } from './tools/SelectTool' +import { EraseTool } from './tools/EraseTool' +import { TextTool } from './tools/TextTool' +import { DrawTool } from './tools/DrawTool' +import { EllipseTool } from './tools/EllipseTool' +import { RectangleTool } from './tools/RectangleTool' +import { ArrowTool } from './tools/ArrowTool' +import { StickyTool } from './tools/StickyTool' const uuid = Utils.uniqueId() -export interface TLDrawCallbacks { +export interface TDCallbacks { /** * (optional) A callback to run when the component mounts. */ - onMount?: (state: TLDrawState) => void + onMount?: (state: TldrawApp) => void /** * (optional) A callback to run when the component's state changes. */ - onChange?: (state: TLDrawState, reason?: string) => void + onChange?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut. */ - onNewProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void /** * (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut. */ - onSaveProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void /** * (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut. */ - onSaveProjectAs?: (state: TLDrawState, e?: KeyboardEvent) => void + onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void /** * (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut. */ - onOpenProject?: (state: TLDrawState, e?: KeyboardEvent) => void + onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void /** * (optional) A callback to run when the user signs in via the menu. */ - onSignIn?: (state: TLDrawState) => void + onSignIn?: (state: TldrawApp) => void /** * (optional) A callback to run when the user signs out via the menu. */ - onSignOut?: (state: TLDrawState) => void + onSignOut?: (state: TldrawApp) => void /** * (optional) A callback to run when the user creates a new project. */ - onUserChange?: (state: TLDrawState, user: TLDrawUser) => void + onUserChange?: (state: TldrawApp, user: TDUser) => void /** * (optional) A callback to run when the state is patched. */ - onPatch?: (state: TLDrawState, reason?: string) => void + onPatch?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the state is changed with a command. */ - onCommand?: (state: TLDrawState, reason?: string) => void + onCommand?: (state: TldrawApp, reason?: string) => void /** * (optional) A callback to run when the state is persisted. */ - onPersist?: (state: TLDrawState) => void + onPersist?: (state: TldrawApp) => void /** * (optional) A callback to run when the user undos. */ - onUndo?: (state: TLDrawState) => void + onUndo?: (state: TldrawApp) => void /** * (optional) A callback to run when the user redos. */ - onRedo?: (state: TLDrawState) => void + onRedo?: (state: TldrawApp) => void } -export class TLDrawState extends StateManager { - public callbacks: TLDrawCallbacks = {} +export class TldrawApp extends StateManager { + callbacks: TDCallbacks = {} - selectHistory: SelectHistory = { - stack: [[]], - pointer: 0, + tools = { + select: new SelectTool(this), + erase: new EraseTool(this), + [TDShapeType.Text]: new TextTool(this), + [TDShapeType.Draw]: new DrawTool(this), + [TDShapeType.Ellipse]: new EllipseTool(this), + [TDShapeType.Rectangle]: new RectangleTool(this), + [TDShapeType.Arrow]: new ArrowTool(this), + [TDShapeType.Sticky]: new StickyTool(this), } - private clipboard?: { - shapes: TLDrawShape[] - bindings: TLDrawBinding[] - } - - private tools = createTools(this) - currentTool: BaseTool = this.tools.select + session?: TldrawSession + + readOnly = false + + isDirty = false + + isCreating = false + + originPoint = [0, 0] + + currentPoint = [0, 0] + + previousPoint = [0, 0] + + shiftKey = false + + altKey = false + + metaKey = false + + ctrlKey = false + + spaceKey = false + editingStartTime = -1 - private isCreating = false + fileSystemHandle: FileSystemHandle | null = null - // The editor's bounding client rect - private bounds: TLBounds = { - minX: 0, - minY: 0, - maxX: 640, - maxY: 480, - width: 640, - height: 480, + viewport = Utils.getBoundsFromPoints([ + [0, 0], + [100, 100], + ]) + + rendererBounds = Utils.getBoundsFromPoints([ + [0, 0], + [100, 100], + ]) + + selectHistory = { + stack: [[]] as string[][], + pointer: 0, } - // The most recent pointer location - private pointerPoint: number[] = [0, 0] + clipboard?: { + shapes: TDShape[] + bindings: TDBinding[] + } - private pasteInfo = { + rotationInfo = { + selectedIds: [] as string[], + center: [0, 0], + } + + pasteInfo = { center: [0, 0], offset: [0, 0], } - fileSystemHandle: FileSystemHandle | null = null - readOnly = false - session?: Session - isDirty = false - - constructor(id?: string, callbacks = {} as TLDrawCallbacks) { - super(TLDrawState.defaultState, id, TLDrawState.version, (prev, next, prevVersion) => { + constructor(id?: string, callbacks = {} as TDCallbacks) { + super(TldrawApp.defaultState, id, TldrawApp.version, (prev, next, prevVersion) => { return { ...next, document: migrate( { ...next.document, ...prev.document, version: prevVersion }, - TLDrawState.version + TldrawApp.version ), } }) @@ -187,19 +220,19 @@ export class TLDrawState extends StateManager { try { this.patchState({ appState: { - status: TLDrawStatus.Idle, + status: TDStatus.Idle, }, - document: migrate(this.document, TLDrawState.version), + document: migrate(this.document, TldrawApp.version), }) } catch (e) { console.error('The data appears to be corrupted. Resetting!', e) localStorage.setItem(this.document.id + '_corrupted', JSON.stringify(this.document)) this.patchState({ - ...TLDrawState.defaultState, + ...TldrawApp.defaultState, appState: { - ...TLDrawState.defaultState.appState, - status: TLDrawStatus.Idle, + ...TldrawApp.defaultState.appState, + status: TDStatus.Idle, }, }) } @@ -214,7 +247,7 @@ export class TLDrawState extends StateManager { * @protected * @returns The final state */ - protected cleanup = (state: TLDrawSnapshot, prev: TLDrawSnapshot): TLDrawSnapshot => { + protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => { const data = { ...state } // Remove deleted shapes and bindings (in Commands, these will be set to undefined) @@ -283,9 +316,9 @@ export class TLDrawState extends StateManager { const toShape = page.shapes[binding.toId] const fromShape = page.shapes[binding.fromId] - const toUtils = TLDR.getShapeUtils(toShape) + const toUtils = TLDR.getShapeUtil(toShape) - const fromUtils = TLDR.getShapeUtils(fromShape) + const fromUtils = TLDR.getShapeUtil(fromShape) // We only need to update the binding's "from" shape const fromDelta = fromUtils.onBindingChange?.( @@ -300,7 +333,7 @@ export class TLDrawState extends StateManager { const nextShape = { ...fromShape, ...fromDelta, - } as TLDrawShape + } as TDShape page.shapes[fromShape.id] = nextShape } @@ -378,7 +411,7 @@ export class TLDrawState extends StateManager { if (data.room) { data.room.users[data.room.userId] = { ...data.room.users[data.room.userId], - point: this.pointerPoint, + point: this.currentPoint, selectedIds: currentPageState.selectedIds, } } @@ -404,11 +437,11 @@ export class TLDrawState extends StateManager { return data } - onPatch = (state: TLDrawSnapshot, id?: string) => { + onPatch = (state: TDSnapshot, id?: string) => { this.callbacks.onPatch?.(this, id) } - onCommand = (state: TLDrawSnapshot, id?: string) => { + onCommand = (state: TDSnapshot, id?: string) => { this.clearSelectHistory() this.isDirty = true this.callbacks.onCommand?.(this, id) @@ -420,12 +453,12 @@ export class TLDrawState extends StateManager { } onUndo = () => { - Session.cache.selectedIds = [...this.selectedIds] + this.rotationInfo.selectedIds = [...this.selectedIds] this.callbacks.onUndo?.(this) } onRedo = () => { - Session.cache.selectedIds = [...this.selectedIds] + this.rotationInfo.selectedIds = [...this.selectedIds] this.callbacks.onRedo?.(this) } @@ -438,7 +471,7 @@ export class TLDrawState extends StateManager { * @param state * @param id */ - protected onStateDidChange = (_state: TLDrawSnapshot, id?: string): void => { + protected onStateDidChange = (_state: TDSnapshot, id?: string): void => { this.callbacks.onChange?.(this, id) } @@ -476,11 +509,27 @@ export class TLDrawState extends StateManager { * @param bounds */ updateBounds = (bounds: TLBounds) => { - if (this.readOnly) return - this.bounds = { ...bounds } - if (this.session) { - this.session.updateViewport(this.viewport) - this.session.update(this.state, this.pointerPoint, false, false, false) + this.rendererBounds = bounds + const { point, zoom } = this.pageState.camera + this.updateViewport(point, zoom) + + if (!this.readOnly && this.session) { + this.session.update() + } + } + + updateViewport = (point: number[], zoom: number) => { + const { width, height } = this.rendererBounds + const [minX, minY] = Vec.sub(Vec.div([0, 0], zoom), point) + const [maxX, maxY] = Vec.sub(Vec.div([width, height], zoom), point) + + this.viewport = { + minX, + minY, + maxX, + maxY, + height: maxX - minX, + width: maxY - minY, } } @@ -532,10 +581,7 @@ export class TLDrawState extends StateManager { /** * Set a setting. */ - setSetting = < - T extends keyof TLDrawSnapshot['settings'], - V extends TLDrawSnapshot['settings'][T] - >( + setSetting = ( name: T, value: V | ((value: V) => V) ): this => { @@ -544,7 +590,7 @@ export class TLDrawState extends StateManager { this.patchState( { settings: { - [name]: typeof value === 'function' ? value(this.state.settings[name] as V) : value, + [name]: typeof value === 'function' ? value(this.settings[name] as V) : value, }, }, `settings:${name}` @@ -561,7 +607,7 @@ export class TLDrawState extends StateManager { this.patchState( { settings: { - isFocusMode: !this.state.settings.isFocusMode, + isFocusMode: !this.settings.isFocusMode, }, }, `settings:toggled_focus_mode` @@ -578,7 +624,7 @@ export class TLDrawState extends StateManager { this.patchState( { settings: { - isPenMode: !this.state.settings.isPenMode, + isPenMode: !this.settings.isPenMode, }, }, `settings:toggled_pen_mode` @@ -593,7 +639,7 @@ export class TLDrawState extends StateManager { toggleDarkMode = (): this => { if (this.session) return this this.patchState( - { settings: { isDarkMode: !this.state.settings.isDarkMode } }, + { settings: { isDarkMode: !this.settings.isDarkMode } }, `settings:toggled_dark_mode` ) this.persist() @@ -606,7 +652,7 @@ export class TLDrawState extends StateManager { toggleZoomSnap = () => { if (this.session) return this this.patchState( - { settings: { isZoomSnap: !this.state.settings.isZoomSnap } }, + { settings: { isZoomSnap: !this.settings.isZoomSnap } }, `settings:toggled_zoom_snap` ) this.persist() @@ -619,7 +665,7 @@ export class TLDrawState extends StateManager { toggleDebugMode = () => { if (this.session) return this this.patchState( - { settings: { isDebugMode: !this.state.settings.isDebugMode } }, + { settings: { isDebugMode: !this.settings.isDebugMode } }, `settings:toggled_debug` ) this.persist() @@ -643,7 +689,7 @@ export class TLDrawState extends StateManager { * Select a tool. * @param tool The tool to select, or "select". */ - selectTool = (type: TLDrawToolType): this => { + selectTool = (type: TDToolType): this => { if (this.readOnly || this.session) return this const tool = this.tools[type] @@ -658,9 +704,8 @@ export class TLDrawState extends StateManager { } this.currentTool.onExit() - + tool.previous = this.currentTool.type this.currentTool = tool - this.currentTool.onEnter() return this.patchState( @@ -700,13 +745,11 @@ export class TLDrawState extends StateManager { if (this.session) return this this.session = undefined this.pasteInfo.offset = [0, 0] - - this.tools = createTools(this) this.currentTool = this.tools.select this.resetHistory() .clearSelectHistory() - .loadDocument(migrate(TLDrawState.defaultDocument, TLDrawState.version)) + .loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version)) .persist() return this } @@ -715,7 +758,7 @@ export class TLDrawState extends StateManager { * * @param document */ - updateUsers = (users: TLDrawUser[], isOwnUpdate = false) => { + updateUsers = (users: TDUser[], isOwnUpdate = false) => { this.patchState( { room: { @@ -740,7 +783,7 @@ export class TLDrawState extends StateManager { * Merge a new document patch into the current document. * @param document */ - mergeDocument = (document: TLDrawDocument): this => { + mergeDocument = (document: TDDocument): this => { // If it's a new document, do a full change. if (this.document.id !== document.id) { this.replaceState({ @@ -749,7 +792,7 @@ export class TLDrawState extends StateManager { ...this.appState, currentPageId: Object.keys(document.pages)[0], }, - document: migrate(document, TLDrawState.version), + document: migrate(document, TldrawApp.version), }) return this } @@ -805,9 +848,6 @@ export class TLDrawState extends StateManager { const { editingId } = this.pageState if (editingId) { - console.warn('A change occured while creating a shape') - if (!editingId) throw Error('Huh?') - document.pages[this.currentPageId].shapes[editingId] = this.page.shapes[editingId] currentPageStates[this.currentPageId].selectedIds = [editingId] } @@ -817,7 +857,7 @@ export class TLDrawState extends StateManager { ...this.state, appState: nextAppState, document: { - ...migrate(document, TLDrawState.version), + ...migrate(document, TldrawApp.version), pageStates: currentPageStates, }, }, @@ -829,7 +869,7 @@ export class TLDrawState extends StateManager { * Update the current document. * @param document */ - updateDocument = (document: TLDrawDocument, reason = 'updated_document'): this => { + updateDocument = (document: TDDocument, reason = 'updated_document'): this => { const prevState = this.state const nextState = { ...prevState, document: { ...prevState.document } } @@ -904,17 +944,18 @@ export class TLDrawState extends StateManager { * Load a new document. * @param document The document to load */ - loadDocument = (document: TLDrawDocument): this => { + loadDocument = (document: TDDocument): this => { this.selectNone() this.resetHistory() this.clearSelectHistory() this.session = undefined + this.replaceState( { - ...TLDrawState.defaultState, - document: migrate(document, TLDrawState.version), + ...TldrawApp.defaultState, + document: migrate(document, TldrawApp.version), appState: { - ...TLDrawState.defaultState.appState, + ...TldrawApp.defaultState.appState, currentPageId: Object.keys(document.pages)[0], }, }, @@ -1005,7 +1046,7 @@ export class TLDrawState extends StateManager { /** * Get the current app state. */ - getAppState = (): TLDrawSnapshot['appState'] => { + getAppState = (): TDSnapshot['appState'] => { return this.appState } @@ -1013,7 +1054,7 @@ export class TLDrawState extends StateManager { * Get a page. * @param pageId (optional) The page's id. */ - getPage = (pageId = this.currentPageId): TLDrawPage => { + getPage = (pageId = this.currentPageId): TDPage => { return TLDR.getPage(this.state, pageId || this.currentPageId) } @@ -1021,7 +1062,7 @@ export class TLDrawState extends StateManager { * Get the shapes (as an array) from a given page. * @param pageId (optional) The page's id. */ - getShapes = (pageId = this.currentPageId): TLDrawShape[] => { + getShapes = (pageId = this.currentPageId): TDShape[] => { return TLDR.getShapes(this.state, pageId || this.currentPageId) } @@ -1029,7 +1070,7 @@ export class TLDrawState extends StateManager { * Get the bindings from a given page. * @param pageId (optional) The page's id. */ - getBindings = (pageId = this.currentPageId): TLDrawBinding[] => { + getBindings = (pageId = this.currentPageId): TDBinding[] => { return TLDR.getBindings(this.state, pageId || this.currentPageId) } @@ -1038,7 +1079,7 @@ export class TLDrawState extends StateManager { * @param id The shape's id. * @param pageId (optional) The page's id. */ - getShape = (id: string, pageId = this.currentPageId): T => { + getShape = (id: string, pageId = this.currentPageId): T => { return TLDR.getShape(this.state, id, pageId) } @@ -1056,7 +1097,7 @@ export class TLDrawState extends StateManager { * @param id The binding's id. * @param pageId (optional) The page's id. */ - getBinding = (id: string, pageId = this.currentPageId): TLDrawBinding => { + getBinding = (id: string, pageId = this.currentPageId): TDBinding => { return TLDR.getBinding(this.state, id, pageId) } @@ -1088,21 +1129,28 @@ export class TLDrawState extends StateManager { /** * Replace the current history stack. */ - set history(commands: TLDrawCommand[]) { + set history(commands: TldrawCommand[]) { this.replaceHistory(commands) } /** * The current document. */ - get document(): TLDrawDocument { + get document(): TDDocument { return this.state.document } /** * The current app state. */ - get appState(): TLDrawSnapshot['appState'] { + get settings(): TDSnapshot['settings'] { + return this.state.settings + } + + /** + * The current app state. + */ + get appState(): TDSnapshot['appState'] { return this.state.appState } @@ -1116,21 +1164,21 @@ export class TLDrawState extends StateManager { /** * The current page. */ - get page(): TLDrawPage { + get page(): TDPage { return this.state.document.pages[this.currentPageId] } /** * The current page's shapes (as an array). */ - get shapes(): TLDrawShape[] { + get shapes(): TDShape[] { return Object.values(this.page.shapes) } /** * The current page's bindings. */ - get bindings(): TLDrawBinding[] { + get bindings(): TDBinding[] { return Object.values(this.page.bindings) } @@ -1158,9 +1206,8 @@ export class TLDrawState extends StateManager { */ createPage = (id?: string): this => { if (this.readOnly) return this - return this.setState( - Commands.createPage(this.state, [-this.bounds.width / 2, -this.bounds.height / 2], id) - ) + const { width, height } = this.rendererBounds + return this.setState(Commands.createPage(this, [-width / 2, -height / 2], id)) } /** @@ -1168,7 +1215,7 @@ export class TLDrawState extends StateManager { * @param pageId The new current page's id. */ changePage = (pageId: string): this => { - return this.setState(Commands.changePage(this.state, pageId)) + return this.setState(Commands.changePage(this, pageId)) } /** @@ -1178,7 +1225,7 @@ export class TLDrawState extends StateManager { */ renamePage = (pageId: string, name: string): this => { if (this.readOnly) return this - return this.setState(Commands.renamePage(this.state, pageId, name)) + return this.setState(Commands.renamePage(this, pageId, name)) } /** @@ -1187,9 +1234,7 @@ export class TLDrawState extends StateManager { */ duplicatePage = (pageId: string): this => { if (this.readOnly) return this - return this.setState( - Commands.duplicatePage(this.state, [-this.bounds.width / 2, -this.bounds.height / 2], pageId) - ) + return this.setState(Commands.duplicatePage(this, pageId)) } /** @@ -1199,7 +1244,7 @@ export class TLDrawState extends StateManager { deletePage = (pageId?: string): this => { if (this.readOnly) return this if (Object.values(this.document.pages).length <= 1) return this - return this.setState(Commands.deletePage(this.state, pageId ? pageId : this.currentPageId)) + return this.setState(Commands.deletePage(this, pageId ? pageId : this.currentPageId)) } /* -------------------------------------------------- */ @@ -1221,7 +1266,7 @@ export class TLDrawState extends StateManager { if (copyingShapes.length === 0) return this - const copyingBindings: TLDrawBinding[] = Object.values(this.page.bindings).filter( + const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter( (binding) => copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId) ) @@ -1272,7 +1317,7 @@ export class TLDrawState extends StateManager { */ paste = (point?: number[]) => { if (this.readOnly) return - const pasteInCurrentPage = (shapes: TLDrawShape[], bindings: TLDrawBinding[]) => { + const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[]) => { const idsMap: Record = {} shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId())) @@ -1329,8 +1374,8 @@ export class TLDrawState extends StateManager { ) { center = Vec.add(center, this.pasteInfo.offset) this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [ - this.state.settings.nudgeDistanceLarge, - this.state.settings.nudgeDistanceLarge, + this.settings.nudgeDistanceLarge, + this.settings.nudgeDistanceLarge, ]) } else { this.pasteInfo.center = center @@ -1346,7 +1391,7 @@ export class TLDrawState extends StateManager { this.create( shapesToPaste.map((shape) => - TLDR.getShapeUtils(shape.type).create({ + TLDR.getShapeUtil(shape.type).create({ ...shape, point: Vec.round(Vec.add(shape.point, delta)), parentId: shape.parentId || this.currentPageId, @@ -1362,11 +1407,11 @@ export class TLDrawState extends StateManager { navigator.clipboard.readText().then((result) => { try { - const data: { type: string; shapes: TLDrawShape[]; bindings: TLDrawBinding[] } = + const data: { type: string; shapes: TDShape[]; bindings: TDBinding[] } = JSON.parse(result) if (data.type !== 'tldr/clipboard') { - throw Error('The pasted string was not from the tldraw clipboard.') + throw Error('The pasted string was not from the Tldraw clipboard.') } pasteInCurrentPage(data.shapes, data.bindings) @@ -1377,9 +1422,9 @@ export class TLDrawState extends StateManager { this.createShapes({ id: shapeId, - type: TLDrawShapeType.Text, + type: TDShapeType.Text, parentId: this.appState.currentPageId, - text: result, + text: TLDR.normalizeText(result), point: this.getPagePoint(this.centerPoint, this.currentPageId), style: { ...this.appState.currentStyle }, }) @@ -1413,7 +1458,7 @@ export class TLDrawState extends StateManager { const shapes = ids.map((id) => this.getShape(id, pageId)) - function getSvgElementForShape(shape: TLDrawShape) { + function getSvgElementForShape(shape: TDShape) { const elm = document.getElementById(shape.id + '_svg') if (!elm) return @@ -1422,7 +1467,7 @@ export class TLDrawState extends StateManager { const element = elm?.cloneNode(true) as SVGElement - const bounds = TLDR.getShapeUtils(shape).getBounds(shape) + const bounds = TLDR.getShapeUtil(shape).getBounds(shape) element.setAttribute( 'transform', @@ -1513,6 +1558,7 @@ export class TLDrawState extends StateManager { * @param reason Why did the camera change? */ setCamera = (point: number[], zoom: number, reason: string): this => { + this.updateViewport(point, zoom) this.patchState( { document: { @@ -1524,10 +1570,6 @@ export class TLDrawState extends StateManager { reason ) - if (this.session) { - this.session.updateViewport(this.viewport) - } - return this } @@ -1596,16 +1638,18 @@ export class TLDrawState extends StateManager { * Zoom to fit the page's shapes. */ zoomToFit = (): this => { - const shapes = this.getShapes() + const shapes = this.shapes if (shapes.length === 0) return this - const bounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds)) + const { rendererBounds } = this + + const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds)) let zoom = TLDR.getCameraZoom( Math.min( - (this.bounds.width - FIT_TO_SCREEN_PADDING) / bounds.width, - (this.bounds.height - FIT_TO_SCREEN_PADDING) / bounds.height + (rendererBounds.width - FIT_TO_SCREEN_PADDING) / commonBounds.width, + (rendererBounds.height - FIT_TO_SCREEN_PADDING) / commonBounds.height ) ) @@ -1614,11 +1658,11 @@ export class TLDrawState extends StateManager { ? Math.min(1, zoom) : zoom - const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom - const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom + const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom + const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom return this.setCamera( - Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])), + Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])), zoom, `zoomed_to_fit` ) @@ -1630,12 +1674,13 @@ export class TLDrawState extends StateManager { zoomToSelection = (): this => { if (this.selectedIds.length === 0) return this - const bounds = TLDR.getSelectedBounds(this.state) + const { rendererBounds } = this + const selectedBounds = TLDR.getSelectedBounds(this.state) let zoom = TLDR.getCameraZoom( Math.min( - (this.bounds.width - FIT_TO_SCREEN_PADDING) / bounds.width, - (this.bounds.height - FIT_TO_SCREEN_PADDING) / bounds.height + (rendererBounds.width - FIT_TO_SCREEN_PADDING) / selectedBounds.width, + (rendererBounds.height - FIT_TO_SCREEN_PADDING) / selectedBounds.height ) ) @@ -1644,11 +1689,11 @@ export class TLDrawState extends StateManager { ? Math.min(1, zoom) : zoom - const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom - const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom + const mx = (rendererBounds.width - selectedBounds.width * zoom) / 2 / zoom + const my = (rendererBounds.height - selectedBounds.height * zoom) / 2 / zoom return this.setCamera( - Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])), + Vec.round(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])), zoom, `zoomed_to_selection` ) @@ -1658,20 +1703,20 @@ export class TLDrawState extends StateManager { * Zoom back to content when the canvas is empty. */ zoomToContent = (): this => { - const shapes = this.getShapes() + const shapes = this.shapes const pageState = this.pageState if (shapes.length === 0) return this - const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds)) - + const { rendererBounds } = this const { zoom } = pageState.camera + const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds)) - const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom - const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom + const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom + const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom return this.setCamera( - Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])), + Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])), this.pageState.camera.zoom, `zoomed_to_content` ) @@ -1831,7 +1876,7 @@ export class TLDrawState extends StateManager { * @param session The new session * @param args arguments of the session's start method. */ - startSession = (type: T, ...args: ExceptFirstTwo>): this => { + startSession = (type: T, ...args: SessionArgsOfType): this => { if (this.readOnly && type !== SessionType.Brush) return this if (this.session) { throw Error(`Already in a session! (${this.session.constructor.name})`) @@ -1839,22 +1884,13 @@ export class TLDrawState extends StateManager { const Session = getSession(type) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - this.session = new Session(this.state, this.viewport, ...args) + this.session = new Session(this, ...args) - const result = this.session.start(this.state) + const result = this.session.start() if (result) { - this.patchState( - { - ...result, - appState: { - ...result.appState, - }, - }, - `session:start_${this.session.constructor.name}` - ) + this.patchState(result, `session:start_${this.session.constructor.name}`) } return this @@ -1865,13 +1901,13 @@ export class TLDrawState extends StateManager { * updateSession. * @param args The arguments of the current session's update method. */ - updateSession = (...args: ExceptFirst>): this => { + updateSession = (): this => { const { session } = this if (!session) return this // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const patch = session.update(this.state, ...args) + const patch = session.update() if (!patch) return this return this.patchState(patch, `session:${session?.constructor.name}`) } @@ -1885,7 +1921,7 @@ export class TLDrawState extends StateManager { if (!session) return this this.session = undefined - const result = session.cancel(this.state) + const result = session.cancel() if (result) { this.patchState(result, `session:cancel:${session.constructor.name}`) @@ -1903,7 +1939,7 @@ export class TLDrawState extends StateManager { if (!session) return this this.session = undefined - const result = session.complete(this.state) + const result = session.complete() if (result === undefined) { this.isCreating = false @@ -1911,7 +1947,7 @@ export class TLDrawState extends StateManager { return this.patchState( { appState: { - status: TLDrawStatus.Idle, + status: TDStatus.Idle, }, document: { pageStates: { @@ -1935,7 +1971,7 @@ export class TLDrawState extends StateManager { result.before = { appState: { ...result.before.appState, - status: TLDrawStatus.Idle, + status: TDStatus.Idle, }, document: { pages: { @@ -1964,7 +2000,7 @@ export class TLDrawState extends StateManager { result.after.appState = { ...result.after.appState, - status: TLDrawStatus.Idle, + status: TDStatus.Idle, } result.after.document = { @@ -1985,7 +2021,7 @@ export class TLDrawState extends StateManager { ...result, appState: { ...result.appState, - status: TLDrawStatus.Idle, + status: TDStatus.Idle, }, document: { pageStates: { @@ -2000,12 +2036,6 @@ export class TLDrawState extends StateManager { ) } - const { isToolLocked, activeTool } = this.appState - - if (!isToolLocked && activeTool !== TLDrawShapeType.Draw) { - this.selectTool('select') - } - return this } @@ -2018,14 +2048,12 @@ export class TLDrawState extends StateManager { * @param shapes An array of shape partials, containing the initial props for the shapes. * @command */ - createShapes = ( - ...shapes: ({ id: string; type: TLDrawShapeType } & Partial)[] - ): this => { + createShapes = (...shapes: ({ id: string; type: TDShapeType } & Partial)[]): this => { if (shapes.length === 0) return this return this.create( shapes.map((shape) => { - return TLDR.getShapeUtils(shape.type).create({ + return TLDR.getShapeUtil(shape.type).create({ parentId: this.currentPageId, ...shape, }) @@ -2038,12 +2066,12 @@ export class TLDrawState extends StateManager { * @param shapes An array of shape partials, containing the changes to be made to each shape. * @command */ - updateShapes = (...shapes: ({ id: string } & Partial)[]): this => { + updateShapes = (...shapes: ({ id: string } & Partial)[]): this => { const pageShapes = this.document.pages[this.currentPageId].shapes const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id]) if (shapesToUpdate.length === 0) return this return this.setState( - Commands.update(this.state, shapesToUpdate, this.currentPageId), + Commands.update(this, shapesToUpdate, this.currentPageId), 'updated_shapes' ) } @@ -2053,17 +2081,17 @@ export class TLDrawState extends StateManager { * @param shapes An array of shape partials, containing the changes to be made to each shape. * @command */ - patchShapes = (...shapes: ({ id: string } & Partial)[]): this => { + patchShapes = (...shapes: ({ id: string } & Partial)[]): this => { const pageShapes = this.document.pages[this.currentPageId].shapes const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id]) if (shapesToUpdate.length === 0) return this return this.patchState( - Commands.update(this.state, shapesToUpdate, this.currentPageId).after, + Commands.update(this, shapesToUpdate, this.currentPageId).after, 'updated_shapes' ) } - createTextShapeAtPoint(point: number[]): this { + createTextShapeAtPoint(point: number[], id?: string): this { const { shapes, appState: { currentPageId, currentStyle }, @@ -2076,19 +2104,20 @@ export class TLDrawState extends StateManager { .filter((shape) => shape.parentId === currentPageId) .sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1 - const id = Utils.uniqueId() - const Text = shapeUtils[TLDrawShapeType.Text] + const Text = shapeUtils[TDShapeType.Text] + const newShape = Text.create({ - id, + id: id || Utils.uniqueId(), parentId: currentPageId, childIndex, point, style: { ...currentStyle }, }) + const bounds = Text.getBounds(newShape) newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2]) this.createShapes(newShape) - this.setEditingId(id) + this.setEditingId(newShape.id) return this } @@ -2098,9 +2127,9 @@ export class TLDrawState extends StateManager { * @param shapes An array of shapes. * @command */ - create = (shapes: TLDrawShape[] = [], bindings: TLDrawBinding[] = []): this => { + create = (shapes: TDShape[] = [], bindings: TDBinding[] = []): this => { if (shapes.length === 0) return this - return this.setState(Commands.createShapes(this.state, shapes, bindings)) + return this.setState(Commands.createShapes(this, shapes, bindings)) } /** @@ -2108,9 +2137,9 @@ export class TLDrawState extends StateManager { * @param shapes * @param bindings */ - patchCreate = (shapes: TLDrawShape[] = [], bindings: TLDrawBinding[] = []): this => { + patchCreate = (shapes: TDShape[] = [], bindings: TDBinding[] = []): this => { if (shapes.length === 0) return this - return this.patchState(Commands.createShapes(this.state, shapes, bindings).after) + return this.patchState(Commands.createShapes(this, shapes, bindings).after) } /** @@ -2120,7 +2149,7 @@ export class TLDrawState extends StateManager { */ delete = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.deleteShapes(this.state, ids)) + return this.setState(Commands.deleteShapes(this, ids)) } /** @@ -2138,7 +2167,7 @@ export class TLDrawState extends StateManager { * @param ids The ids of the shapes to change (defaults to selection). */ style = (style: Partial, ids = this.selectedIds): this => { - return this.setState(Commands.styleShapes(this.state, ids, style)) + return this.setState(Commands.styleShapes(this, ids, style)) } /** @@ -2148,7 +2177,7 @@ export class TLDrawState extends StateManager { */ align = (type: AlignType, ids = this.selectedIds): this => { if (ids.length < 2) return this - return this.setState(Commands.alignShapes(this.state, ids, type)) + return this.setState(Commands.alignShapes(this, ids, type)) } /** @@ -2158,7 +2187,7 @@ export class TLDrawState extends StateManager { */ distribute = (direction: DistributeType, ids = this.selectedIds): this => { if (ids.length < 3) return this - return this.setState(Commands.distributeShapes(this.state, ids, direction)) + return this.setState(Commands.distributeShapes(this, ids, direction)) } /** @@ -2168,7 +2197,7 @@ export class TLDrawState extends StateManager { */ stretch = (direction: StretchType, ids = this.selectedIds): this => { if (ids.length < 2) return this - return this.setState(Commands.stretchShapes(this.state, ids, direction)) + return this.setState(Commands.stretchShapes(this, ids, direction)) } /** @@ -2177,7 +2206,7 @@ export class TLDrawState extends StateManager { */ flipHorizontal = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.flipShapes(this.state, ids, FlipType.Horizontal)) + return this.setState(Commands.flipShapes(this, ids, FlipType.Horizontal)) } /** @@ -2186,7 +2215,7 @@ export class TLDrawState extends StateManager { */ flipVertical = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.flipShapes(this.state, ids, FlipType.Vertical)) + return this.setState(Commands.flipShapes(this, ids, FlipType.Vertical)) } /** @@ -2201,7 +2230,8 @@ export class TLDrawState extends StateManager { ids = this.selectedIds ): this => { if (ids.length === 0) return this - this.setState(Commands.moveShapesToPage(this.state, ids, this.bounds, fromPageId, toPageId)) + const { rendererBounds } = this + this.setState(Commands.moveShapesToPage(this, ids, rendererBounds, fromPageId, toPageId)) return this } @@ -2211,7 +2241,7 @@ export class TLDrawState extends StateManager { */ moveToBack = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.reorderShapes(this.state, ids, MoveType.ToBack)) + return this.setState(Commands.reorderShapes(this, ids, MoveType.ToBack)) } /** @@ -2220,7 +2250,7 @@ export class TLDrawState extends StateManager { */ moveBackward = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.reorderShapes(this.state, ids, MoveType.Backward)) + return this.setState(Commands.reorderShapes(this, ids, MoveType.Backward)) } /** @@ -2229,7 +2259,7 @@ export class TLDrawState extends StateManager { */ moveForward = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.reorderShapes(this.state, ids, MoveType.Forward)) + return this.setState(Commands.reorderShapes(this, ids, MoveType.Forward)) } /** @@ -2238,7 +2268,7 @@ export class TLDrawState extends StateManager { */ moveToFront = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.reorderShapes(this.state, ids, MoveType.ToFront)) + return this.setState(Commands.reorderShapes(this, ids, MoveType.ToFront)) } /** @@ -2249,9 +2279,7 @@ export class TLDrawState extends StateManager { */ nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState( - Commands.translateShapes(this.state, ids, Vec.mul(delta, isMajor ? 10 : 1)) - ) + return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, isMajor ? 10 : 1))) } /** @@ -2261,7 +2289,7 @@ export class TLDrawState extends StateManager { duplicate = (ids = this.selectedIds, point?: number[]): this => { if (this.readOnly) return this if (ids.length === 0) return this - return this.setState(Commands.duplicateShapes(this.state, ids, point)) + return this.setState(Commands.duplicateShapes(this, ids, point)) } /** @@ -2271,8 +2299,8 @@ export class TLDrawState extends StateManager { * @param ids The ids to change (defaults to selection). */ resetBounds = (ids = this.selectedIds): this => { - const command = Commands.resetBounds(this.state, ids, this.currentPageId) - return this.setState(Commands.resetBounds(this.state, ids, this.currentPageId), command.id) + const command = Commands.resetBounds(this, ids, this.currentPageId) + return this.setState(Commands.resetBounds(this, ids, this.currentPageId), command.id) } /** @@ -2281,7 +2309,7 @@ export class TLDrawState extends StateManager { */ toggleHidden = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.toggleShapeProp(this.state, ids, 'isHidden')) + return this.setState(Commands.toggleShapeProp(this, ids, 'isHidden')) } /** @@ -2290,7 +2318,7 @@ export class TLDrawState extends StateManager { */ toggleLocked = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.toggleShapeProp(this.state, ids, 'isLocked')) + return this.setState(Commands.toggleShapeProp(this, ids, 'isLocked')) } /** @@ -2299,7 +2327,7 @@ export class TLDrawState extends StateManager { */ toggleAspectRatioLocked = (ids = this.selectedIds): this => { if (ids.length === 0) return this - return this.setState(Commands.toggleShapeProp(this.state, ids, 'isAspectRatioLocked')) + return this.setState(Commands.toggleShapeProp(this, ids, 'isAspectRatioLocked')) } /** @@ -2309,7 +2337,7 @@ export class TLDrawState extends StateManager { */ toggleDecoration = (handleId: string, ids = this.selectedIds): this => { if (ids.length === 0 || !(handleId === 'start' || handleId === 'end')) return this - return this.setState(Commands.toggleShapesDecoration(this.state, ids, handleId)) + return this.setState(Commands.toggleShapesDecoration(this, ids, handleId)) } /** @@ -2319,7 +2347,7 @@ export class TLDrawState extends StateManager { */ rotate = (delta = Math.PI * -0.5, ids = this.selectedIds): this => { if (ids.length === 0) return this - const change = Commands.rotateShapes(this.state, ids, delta) + const change = Commands.rotateShapes(this, ids, delta) if (!change) return this return this.setState(change) } @@ -2336,13 +2364,13 @@ export class TLDrawState extends StateManager { ): this => { if (this.readOnly) return this - if (ids.length === 1 && this.getShape(ids[0], pageId).type === TLDrawShapeType.Group) { + if (ids.length === 1 && this.getShape(ids[0], pageId).type === TDShapeType.Group) { return this.ungroup(ids, pageId) } if (ids.length < 2) return this - const command = Commands.groupShapes(this.state, ids, groupId, pageId) + const command = Commands.groupShapes(this, ids, groupId, pageId) if (!command) return this return this.setState(command) } @@ -2356,11 +2384,11 @@ export class TLDrawState extends StateManager { const groups = ids .map((id) => this.getShape(id, pageId)) - .filter((shape) => shape.type === TLDrawShapeType.Group) + .filter((shape) => shape.type === TDShapeType.Group) if (groups.length === 0) return this - const command = Commands.ungroupShapes(this.state, ids, groups as GroupShape[], pageId) + const command = Commands.ungroupShapes(this, ids, groups as GroupShape[], pageId) if (!command) return this return this.setState(command) } @@ -2380,9 +2408,27 @@ export class TLDrawState extends StateManager { /* ----------------- Keyboard Events ---------------- */ onKeyDown: TLKeyboardEventHandler = (key, info, e) => { - if (key === 'Escape') { - this.cancel() - return + switch (e.key) { + case 'Escape': { + this.cancel() + break + } + case 'Meta': { + this.metaKey = true + break + } + case 'Alt': { + this.altKey = true + break + } + case 'Control': { + this.ctrlKey = true + break + } + case ' ': { + this.spaceKey = true + break + } } this.currentTool.onKeyDown?.(key, info, e) @@ -2393,6 +2439,25 @@ export class TLDrawState extends StateManager { onKeyUp: TLKeyboardEventHandler = (key, info, e) => { if (!info) return + switch (e.key) { + case 'Meta': { + this.metaKey = false + break + } + case 'Alt': { + this.altKey = false + break + } + case 'Control': { + this.ctrlKey = false + break + } + case ' ': { + this.spaceKey = false + break + } + } + this.currentTool.onKeyUp?.(key, info, e) } @@ -2424,19 +2489,36 @@ export class TLDrawState extends StateManager { } onZoom: TLWheelEventHandler = (info, e) => { - if (this.state.appState.status !== TLDrawStatus.Idle) return - this.zoomBy(info.delta[2] / 100, info.delta) + if (this.state.appState.status !== TDStatus.Idle) return + + const delta = + e.deltaMode === WheelEvent.DOM_DELTA_PIXEL + ? info.delta[2] / 500 + : e.deltaMode === WheelEvent.DOM_DELTA_LINE + ? info.delta[2] / 100 + : info.delta[2] / 2 + + this.zoomBy(delta, info.delta) this.onPointerMove(info, e as unknown as React.PointerEvent) } /* ----------------- Pointer Events ----------------- */ + updateInputs: TLPointerEventHandler = (info) => { + this.currentPoint = [...this.getPagePoint(info.point), info.pressure] + this.shiftKey = info.shiftKey + this.altKey = info.altKey + this.ctrlKey = info.ctrlKey + this.metaKey = info.metaKey + } + onPointerMove: TLPointerEventHandler = (info, e) => { + this.previousPoint = this.currentPoint + this.updateInputs(info, e) + // Several events (e.g. pan) can trigger the same "pointer move" behavior this.currentTool.onPointerMove?.(info, e) - this.pointerPoint = this.getPagePoint(info.point) - // Move this to an emitted event if (this.state.room) { const { users, userId } = this.state.room @@ -2448,97 +2530,200 @@ export class TLDrawState extends StateManager { } } - onPointerDown: TLPointerEventHandler = (...args) => this.currentTool.onPointerDown?.(...args) + onPointerDown: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onPointerDown?.(info, e) + } - onPointerUp: TLPointerEventHandler = (...args) => this.currentTool.onPointerUp?.(...args) + onPointerUp: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onPointerUp?.(info, e) + } // Canvas (background) - onPointCanvas: TLCanvasEventHandler = (...args) => this.currentTool.onPointCanvas?.(...args) + onPointCanvas: TLCanvasEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onPointCanvas?.(info, e) + } - onDoubleClickCanvas: TLCanvasEventHandler = (...args) => - this.currentTool.onDoubleClickCanvas?.(...args) + onDoubleClickCanvas: TLCanvasEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDoubleClickCanvas?.(info, e) + } - onRightPointCanvas: TLCanvasEventHandler = (...args) => - this.currentTool.onRightPointCanvas?.(...args) + onRightPointCanvas: TLCanvasEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onRightPointCanvas?.(info, e) + } - onDragCanvas: TLCanvasEventHandler = (...args) => this.currentTool.onDragCanvas?.(...args) + onDragCanvas: TLCanvasEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDragCanvas?.(info, e) + } - onReleaseCanvas: TLCanvasEventHandler = (...args) => this.currentTool.onReleaseCanvas?.(...args) + onReleaseCanvas: TLCanvasEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onReleaseCanvas?.(info, e) + } // Shape - onPointShape: TLPointerEventHandler = (...args) => this.currentTool.onPointShape?.(...args) + onPointShape: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onPointShape?.(info, e) + } - onReleaseShape: TLPointerEventHandler = (...args) => this.currentTool.onReleaseShape?.(...args) + onReleaseShape: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onReleaseShape?.(info, e) + } - onDoubleClickShape: TLPointerEventHandler = (...args) => - this.currentTool.onDoubleClickShape?.(...args) + onDoubleClickShape: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onDoubleClickShape?.(info, e) + } - onRightPointShape: TLPointerEventHandler = (...args) => - this.currentTool.onRightPointShape?.(...args) + onRightPointShape: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onRightPointShape?.(info, e) + } - onDragShape: TLPointerEventHandler = (...args) => this.currentTool.onDragShape?.(...args) + onDragShape: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDragShape?.(info, e) + } - onHoverShape: TLPointerEventHandler = (...args) => this.currentTool.onHoverShape?.(...args) + onHoverShape: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onHoverShape?.(info, e) + } - onUnhoverShape: TLPointerEventHandler = (...args) => this.currentTool.onUnhoverShape?.(...args) + onUnhoverShape: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onUnhoverShape?.(info, e) + } // Bounds (bounding box background) - onPointBounds: TLBoundsEventHandler = (...args) => this.currentTool.onPointBounds?.(...args) + onPointBounds: TLBoundsEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onPointBounds?.(info, e) + } - onDoubleClickBounds: TLBoundsEventHandler = (...args) => - this.currentTool.onDoubleClickBounds?.(...args) + onDoubleClickBounds: TLBoundsEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onDoubleClickBounds?.(info, e) + } - onRightPointBounds: TLBoundsEventHandler = (...args) => - this.currentTool.onRightPointBounds?.(...args) + onRightPointBounds: TLBoundsEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onRightPointBounds?.(info, e) + } - onDragBounds: TLBoundsEventHandler = (...args) => this.currentTool.onDragBounds?.(...args) + onDragBounds: TLBoundsEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDragBounds?.(info, e) + } - onHoverBounds: TLBoundsEventHandler = (...args) => this.currentTool.onHoverBounds?.(...args) + onHoverBounds: TLBoundsEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onHoverBounds?.(info, e) + } - onUnhoverBounds: TLBoundsEventHandler = (...args) => this.currentTool.onUnhoverBounds?.(...args) + onUnhoverBounds: TLBoundsEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onUnhoverBounds?.(info, e) + } - onReleaseBounds: TLBoundsEventHandler = (...args) => this.currentTool.onReleaseBounds?.(...args) + onReleaseBounds: TLBoundsEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onReleaseBounds?.(info, e) + } // Bounds handles (corners, edges) - onPointBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onPointBoundsHandle?.(...args) + onPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onPointBoundsHandle?.(info, e) + } - onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onDoubleClickBoundsHandle?.(...args) + onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onDoubleClickBoundsHandle?.(info, e) + } - onRightPointBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onRightPointBoundsHandle?.(...args) + onRightPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onRightPointBoundsHandle?.(info, e) + } - onDragBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onDragBoundsHandle?.(...args) + onDragBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDragBoundsHandle?.(info, e) + } - onHoverBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onHoverBoundsHandle?.(...args) + onHoverBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onHoverBoundsHandle?.(info, e) + } - onUnhoverBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onUnhoverBoundsHandle?.(...args) + onUnhoverBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onUnhoverBoundsHandle?.(info, e) + } - onReleaseBoundsHandle: TLBoundsHandleEventHandler = (...args) => - this.currentTool.onReleaseBoundsHandle?.(...args) + onReleaseBoundsHandle: TLBoundsHandleEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onReleaseBoundsHandle?.(info, e) + } // Handles (ie the handles of a selected arrow) - onPointHandle: TLPointerEventHandler = (...args) => this.currentTool.onPointHandle?.(...args) + onPointHandle: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onPointHandle?.(info, e) + } - onDoubleClickHandle: TLPointerEventHandler = (...args) => - this.currentTool.onDoubleClickHandle?.(...args) + onDoubleClickHandle: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onDoubleClickHandle?.(info, e) + } - onRightPointHandle: TLPointerEventHandler = (...args) => - this.currentTool.onRightPointHandle?.(...args) + onRightPointHandle: TLPointerEventHandler = (info, e) => { + this.originPoint = this.getPagePoint(info.point) + this.updateInputs(info, e) + this.currentTool.onRightPointHandle?.(info, e) + } - onDragHandle: TLPointerEventHandler = (...args) => this.currentTool.onDragHandle?.(...args) + onDragHandle: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onDragHandle?.(info, e) + } - onHoverHandle: TLPointerEventHandler = (...args) => this.currentTool.onHoverHandle?.(...args) + onHoverHandle: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onHoverHandle?.(info, e) + } - onUnhoverHandle: TLPointerEventHandler = (...args) => this.currentTool.onUnhoverHandle?.(...args) + onUnhoverHandle: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onUnhoverHandle?.(info, e) + } - onReleaseHandle: TLPointerEventHandler = (...args) => this.currentTool.onReleaseHandle?.(...args) + onReleaseHandle: TLPointerEventHandler = (info, e) => { + this.updateInputs(info, e) + this.currentTool.onReleaseHandle?.(info, e) + } - onShapeChange = (shape: { id: string } & Partial) => { + onShapeChange = (shape: { id: string } & Partial) => { this.updateShapes(shape) } @@ -2552,9 +2737,9 @@ export class TLDrawState extends StateManager { // If we're editing text, then delete the text if it's empty const shape = this.getShape(editingId) this.setEditingId() - if (shape.type === TLDrawShapeType.Text) { + if (shape.type === TDShapeType.Text) { if (shape.text.trim().length <= 0) { - this.setState(Commands.deleteShapes(this.state, [editingId]), 'delete_empty_text') + this.patchState(Commands.deleteShapes(this, [editingId]).after, 'delete_empty_text') } else { this.select(editingId) } @@ -2597,6 +2782,10 @@ export class TLDrawState extends StateManager { return this.selectedIds.includes(id) } + get room() { + return this.state.room + } + get isLocal() { return this.state.room === undefined || this.state.room.id === 'local' } @@ -2610,30 +2799,17 @@ export class TLDrawState extends StateManager { return this.state.room.users[this.state.room.userId] } + // The center of the component (in screen space) get centerPoint() { - return Vec.round([this.bounds.width / 2, this.bounds.height / 2]) + const { width, height } = this.rendererBounds + return Vec.round([width / 2, height / 2]) } - get viewport() { - const { camera } = this.pageState - const { width, height } = this.bounds - - const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point) - const [maxX, maxY] = Vec.sub(Vec.div([width, height], camera.zoom), camera.point) - - return { - minX, - minY, - maxX, - maxY, - height: maxX - minX, - width: maxY - minY, - } - } + getShapeUtil = TLDR.getShapeUtil static version = 13 - static defaultDocument: TLDrawDocument = { + static defaultDocument: TDDocument = { id: 'doc', name: 'New Document', version: 13, @@ -2658,7 +2834,7 @@ export class TLDrawState extends StateManager { }, } - static defaultState: TLDrawSnapshot = { + static defaultState: TDSnapshot = { settings: { isPenMode: false, isDarkMode: false, @@ -2683,22 +2859,9 @@ export class TLDrawState extends StateManager { isToolLocked: false, isStyleOpen: false, isEmptyCanvas: false, - status: TLDrawStatus.Idle, + status: TDStatus.Idle, snapLines: [], }, - document: TLDrawState.defaultDocument, - room: { - id: 'local', - userId: uuid, - users: { - [uuid]: { - id: uuid, - color: USER_COLORS[0], - point: [100, 100], - selectedIds: [], - activeShapes: [], - }, - }, - }, + document: TldrawApp.defaultDocument, } } diff --git a/packages/tldraw/src/state/__snapshots__/TLDrawState.spec.ts.snap b/packages/tldraw/src/state/__snapshots__/TLDrawApp.spec.ts.snap similarity index 62% rename from packages/tldraw/src/state/__snapshots__/TLDrawState.spec.ts.snap rename to packages/tldraw/src/state/__snapshots__/TLDrawApp.spec.ts.snap index dffe46341..59e77a8c2 100644 --- a/packages/tldraw/src/state/__snapshots__/TLDrawState.spec.ts.snap +++ b/packages/tldraw/src/state/__snapshots__/TLDrawApp.spec.ts.snap @@ -1,8 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` 1`] = `"[]"`; +exports[` 1`] = ` +"[ + { + \\"id\\": \\"rect1\\", + \\"parentId\\": \\"page1\\", + \\"name\\": \\"Rectangle\\", + \\"childIndex\\": 1, + \\"type\\": \\"rectangle\\", + \\"point\\": [ + 0, + 0 + ], + \\"size\\": [ + 100, + 100 + ], + \\"style\\": { + \\"dash\\": \\"draw\\", + \\"size\\": \\"medium\\", + \\"color\\": \\"blue\\" + } + }, + { + \\"id\\": \\"rect2\\", + \\"parentId\\": \\"page1\\", + \\"name\\": \\"Rectangle\\", + \\"childIndex\\": 2, + \\"type\\": \\"rectangle\\", + \\"point\\": [ + 100, + 100 + ], + \\"size\\": [ + 100, + 100 + ], + \\"style\\": { + \\"dash\\": \\"draw\\", + \\"size\\": \\"medium\\", + \\"color\\": \\"blue\\" + } + }, + { + \\"id\\": \\"rect3\\", + \\"parentId\\": \\"page1\\", + \\"name\\": \\"Rectangle\\", + \\"childIndex\\": 3, + \\"type\\": \\"rectangle\\", + \\"point\\": [ + 20, + 20 + ], + \\"size\\": [ + 100, + 100 + ], + \\"style\\": { + \\"dash\\": \\"draw\\", + \\"size\\": \\"medium\\", + \\"color\\": \\"blue\\" + } + } +]" +`; -exports[`TLDrawState Exposes undo/redo stack: history 1`] = ` +exports[`TldrawTestApp Exposes undo/redo stack: history 1`] = ` Array [ Object { "after": Object { @@ -36,6 +99,7 @@ Array [ "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "rectangle", @@ -96,6 +160,7 @@ Array [ "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "rectangle", @@ -129,7 +194,7 @@ Array [ ] `; -exports[`TLDrawState Selection When selecting all selects all: selected all 1`] = ` +exports[`TldrawTestApp Selection When selecting all selects all: selected all 1`] = ` Array [ "rect1", "rect2", @@ -137,6 +202,6 @@ Array [ ] `; -exports[`TLDrawState When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `""`; +exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `""`; -exports[`TLDrawState When copying to SVG Copies shapes.: copied svg 1`] = `""`; +exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `""`; diff --git a/packages/tldraw/src/state/commands/alignShapes/alignShapes.spec.ts b/packages/tldraw/src/state/commands/alignShapes/alignShapes.spec.ts index e094eb10e..518a2943e 100644 --- a/packages/tldraw/src/state/commands/alignShapes/alignShapes.spec.ts +++ b/packages/tldraw/src/state/commands/alignShapes/alignShapes.spec.ts @@ -1,17 +1,16 @@ import Vec from '@tldraw/vec' -import { TLDrawState } from '~state' -import { mockDocument, TLDrawStateUtils } from '~test' -import { AlignType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { AlignType, TDShapeType } from '~types' describe('Align command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() describe('when less than two shapes are selected', () => { it('does nothing', () => { - state.loadDocument(mockDocument).select('rect2') - const initialState = state.state - state.align(AlignType.Top) - const currentState = state.state + app.loadDocument(mockDocument).select('rect2') + const initialState = app.state + app.align(AlignType.Top) + const currentState = app.state expect(currentState).toEqual(initialState) }) @@ -19,86 +18,89 @@ describe('Align command', () => { describe('when multiple shapes are selected', () => { beforeEach(() => { - state.loadDocument(mockDocument) - state.selectAll() + app.loadDocument(mockDocument) + app.selectAll() }) it('does, undoes and redoes command', () => { - state.align(AlignType.Top) + app.align(AlignType.Top) - expect(state.getShape('rect2').point).toEqual([100, 0]) + expect(app.getShape('rect2').point).toEqual([100, 0]) - state.undo() + app.undo() - expect(state.getShape('rect2').point).toEqual([100, 100]) + expect(app.getShape('rect2').point).toEqual([100, 100]) - state.redo() + app.redo() - expect(state.getShape('rect2').point).toEqual([100, 0]) + expect(app.getShape('rect2').point).toEqual([100, 0]) }) it('aligns top', () => { - state.align(AlignType.Top) + app.align(AlignType.Top) - expect(state.getShape('rect2').point).toEqual([100, 0]) + expect(app.getShape('rect2').point).toEqual([100, 0]) }) it('aligns right', () => { - state.align(AlignType.Right) + app.align(AlignType.Right) - expect(state.getShape('rect1').point).toEqual([100, 0]) + expect(app.getShape('rect1').point).toEqual([100, 0]) }) it('aligns bottom', () => { - state.align(AlignType.Bottom) + app.align(AlignType.Bottom) - expect(state.getShape('rect1').point).toEqual([0, 100]) + expect(app.getShape('rect1').point).toEqual([0, 100]) }) it('aligns left', () => { - state.align(AlignType.Left) + app.align(AlignType.Left) - expect(state.getShape('rect2').point).toEqual([0, 100]) + expect(app.getShape('rect2').point).toEqual([0, 100]) }) it('aligns center horizontal', () => { - state.align(AlignType.CenterHorizontal) + app.align(AlignType.CenterHorizontal) - expect(state.getShape('rect1').point).toEqual([50, 0]) - expect(state.getShape('rect2').point).toEqual([50, 100]) + expect(app.getShape('rect1').point).toEqual([50, 0]) + expect(app.getShape('rect2').point).toEqual([50, 100]) }) it('aligns center vertical', () => { - state.align(AlignType.CenterVertical) + app.align(AlignType.CenterVertical) - expect(state.getShape('rect1').point).toEqual([0, 50]) - expect(state.getShape('rect2').point).toEqual([100, 50]) + expect(app.getShape('rect1').point).toEqual([0, 50]) + expect(app.getShape('rect2').point).toEqual([100, 50]) }) }) }) describe('when aligning groups', () => { it('aligns children', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( - { id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] }, - { id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] }, - { id: 'rect3', type: TLDrawShapeType.Rectangle, point: [200, 200], size: [100, 100] }, - { id: 'rect4', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [200, 200] } + { id: 'rect1', type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100] }, + { id: 'rect2', type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100] }, + { id: 'rect3', type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100] }, + { id: 'rect4', type: TDShapeType.Rectangle, point: [0, 0], size: [200, 200] } ) .group(['rect1', 'rect2'], 'groupA') .select('rect3', 'rect4') .align(AlignType.CenterVertical) - const p0 = state.getShape('rect4').point - const p1 = state.getShape('rect3').point + const p0 = app.getShape('rect4').point + const p1 = app.getShape('rect3').point - state.undo().delete(['rect4']).selectAll().align(AlignType.CenterVertical) - - new TLDrawStateUtils(state).expectShapesToBeAtPoints({ - rect1: p0, - rect2: Vec.add(p0, [100, 100]), - rect3: p1, - }) + app + .undo() + .delete(['rect4']) + .selectAll() + .align(AlignType.CenterVertical) + .expectShapesToBeAtPoints({ + rect1: p0, + rect2: Vec.add(p0, [100, 100]), + rect3: p1, + }) }) }) diff --git a/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts b/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts index e28966c27..235c8f8b6 100644 --- a/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts +++ b/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts @@ -1,27 +1,27 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Utils } from '@tldraw/core' -import { AlignType, TLDrawCommand, TLDrawShapeType } from '~types' -import type { TLDrawSnapshot } from '~types' +import { AlignType, TldrawCommand, TDShapeType } from '~types' +import type { TDSnapshot } from '~types' import { TLDR } from '~state/TLDR' import Vec from '@tldraw/vec' +import type { TldrawApp } from '../../internal' -export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType): TLDrawCommand { - const { currentPageId } = data.appState +export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand { + const { currentPageId } = app - const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId)) + const initialShapes = ids.map((id) => app.getShape(id)) const boundsForShapes = initialShapes.map((shape) => { return { id: shape.id, point: [...shape.point], - bounds: TLDR.getShapeUtils(shape).getBounds(shape), + bounds: TLDR.getBounds(shape), } }) const commonBounds = Utils.getCommonBounds(boundsForShapes.map(({ bounds }) => bounds)) const midX = commonBounds.minX + commonBounds.width / 2 - const midY = commonBounds.minY + commonBounds.height / 2 const deltaMap = Object.fromEntries( @@ -44,7 +44,7 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType ) const { before, after } = TLDR.mutateShapes( - data, + app.state, ids, (shape) => { if (!deltaMap[shape.id]) return shape @@ -54,11 +54,11 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType ) initialShapes.forEach((shape) => { - if (shape.type === TLDrawShapeType.Group) { + if (shape.type === TDShapeType.Group) { const delta = Vec.sub(after[shape.id].point!, before[shape.id].point!) shape.children.forEach((id) => { - const child = TLDR.getShape(data, id, currentPageId) + const child = app.getShape(id) before[child.id] = { point: child.point } after[child.id] = { point: Vec.add(child.point, delta) } }) diff --git a/packages/tldraw/src/state/commands/changePage/changePage.spec.ts b/packages/tldraw/src/state/commands/changePage/changePage.spec.ts index 4ed72dfd6..5b5e28090 100644 --- a/packages/tldraw/src/state/commands/changePage/changePage.spec.ts +++ b/packages/tldraw/src/state/commands/changePage/changePage.spec.ts @@ -1,32 +1,31 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Change page command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() it('does, undoes and redoes command', () => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) - const initialId = state.page.id + const initialId = app.page.id - state.createPage() + app.createPage() - const nextId = state.page.id + const nextId = app.page.id - state.changePage(initialId) + app.changePage(initialId) - expect(state.page.id).toBe(initialId) + expect(app.page.id).toBe(initialId) - state.changePage(nextId) + app.changePage(nextId) - expect(state.page.id).toBe(nextId) + expect(app.page.id).toBe(nextId) - state.undo() + app.undo() - expect(state.page.id).toBe(initialId) + expect(app.page.id).toBe(initialId) - state.redo() + app.redo() - expect(state.page.id).toBe(nextId) + expect(app.page.id).toBe(nextId) }) }) diff --git a/packages/tldraw/src/state/commands/changePage/changePage.ts b/packages/tldraw/src/state/commands/changePage/changePage.ts index d6c5e4c07..c36ffe9da 100644 --- a/packages/tldraw/src/state/commands/changePage/changePage.ts +++ b/packages/tldraw/src/state/commands/changePage/changePage.ts @@ -1,11 +1,12 @@ -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' +import type { TldrawApp } from '../../internal' -export function changePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand { +export function changePage(app: TldrawApp, pageId: string): TldrawCommand { return { id: 'change_page', before: { appState: { - currentPageId: data.appState.currentPageId, + currentPageId: app.currentPageId, }, }, after: { diff --git a/packages/tldraw/src/state/commands/createPage/createPage.spec.ts b/packages/tldraw/src/state/commands/createPage/createPage.spec.ts index bfea4ec5c..dd34cc5f2 100644 --- a/packages/tldraw/src/state/commands/createPage/createPage.spec.ts +++ b/packages/tldraw/src/state/commands/createPage/createPage.spec.ts @@ -1,34 +1,33 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Create page command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() it('does, undoes and redoes command', () => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) - const initialId = state.page.id - const initialPageState = state.pageState + const initialId = app.page.id + const initialPageState = app.pageState - state.createPage() + app.createPage() - const nextId = state.page.id - const nextPageState = state.pageState + const nextId = app.page.id + const nextPageState = app.pageState - expect(Object.keys(state.document.pages).length).toBe(2) - expect(state.page.id).toBe(nextId) - expect(state.pageState).toEqual(nextPageState) + expect(Object.keys(app.document.pages).length).toBe(2) + expect(app.page.id).toBe(nextId) + expect(app.pageState).toEqual(nextPageState) - state.undo() + app.undo() - expect(Object.keys(state.document.pages).length).toBe(1) - expect(state.page.id).toBe(initialId) - expect(state.pageState).toEqual(initialPageState) + expect(Object.keys(app.document.pages).length).toBe(1) + expect(app.page.id).toBe(initialId) + expect(app.pageState).toEqual(initialPageState) - state.redo() + app.redo() - expect(Object.keys(state.document.pages).length).toBe(2) - expect(state.page.id).toBe(nextId) - expect(state.pageState).toEqual(nextPageState) + expect(Object.keys(app.document.pages).length).toBe(2) + expect(app.page.id).toBe(nextId) + expect(app.pageState).toEqual(nextPageState) }) }) diff --git a/packages/tldraw/src/state/commands/createPage/createPage.ts b/packages/tldraw/src/state/commands/createPage/createPage.ts index 3e4834d16..1aa1aa0cc 100644 --- a/packages/tldraw/src/state/commands/createPage/createPage.ts +++ b/packages/tldraw/src/state/commands/createPage/createPage.ts @@ -1,14 +1,15 @@ -import type { TLDrawSnapshot, TLDrawCommand, TLDrawPage } from '~types' +import type { TldrawCommand, TDPage } from '~types' import { Utils, TLPageState } from '@tldraw/core' +import type { TldrawApp } from '~state' export function createPage( - data: TLDrawSnapshot, + app: TldrawApp, center: number[], pageId = Utils.uniqueId() -): TLDrawCommand { - const { currentPageId } = data.appState +): TldrawCommand { + const { currentPageId } = app - const topPage = Object.values(data.document.pages).sort( + const topPage = Object.values(app.state.document.pages).sort( (a, b) => (b.childIndex || 0) - (a.childIndex || 0) )[0] @@ -17,7 +18,7 @@ export function createPage( // TODO: Iterate the name better const nextName = `New Page` - const page: TLDrawPage = { + const page: TDPage = { id: pageId, name: nextName, childIndex: nextChildIndex, diff --git a/packages/tldraw/src/state/commands/createShapes/createShapes.spec.ts b/packages/tldraw/src/state/commands/createShapes/createShapes.spec.ts index 5a466fc1a..7f4091df0 100644 --- a/packages/tldraw/src/state/commands/createShapes/createShapes.spec.ts +++ b/packages/tldraw/src/state/commands/createShapes/createShapes.spec.ts @@ -1,37 +1,36 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Create command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is provided', () => { it('does nothing', () => { - const initialState = state.state - state.create() + const initialState = app.state + app.create() - const currentState = state.state + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - const shape = { ...state.getShape('rect1'), id: 'rect4' } - state.create([shape]) + const shape = { ...app.getShape('rect1'), id: 'rect4' } + app.create([shape]) - expect(state.getShape('rect4')).toBeTruthy() + expect(app.getShape('rect4')).toBeTruthy() - state.undo() + app.undo() - expect(state.getShape('rect4')).toBe(undefined) + expect(app.getShape('rect4')).toBe(undefined) - state.redo() + app.redo() - expect(state.getShape('rect4')).toBeTruthy() + expect(app.getShape('rect4')).toBeTruthy() }) it.todo('Creates bindings') diff --git a/packages/tldraw/src/state/commands/createShapes/createShapes.ts b/packages/tldraw/src/state/commands/createShapes/createShapes.ts index 2557452d6..b1cae10a5 100644 --- a/packages/tldraw/src/state/commands/createShapes/createShapes.ts +++ b/packages/tldraw/src/state/commands/createShapes/createShapes.ts @@ -1,24 +1,24 @@ import type { Patch } from 'rko' -import { TLDR } from '~state/TLDR' -import type { TLDrawShape, TLDrawSnapshot, TLDrawCommand, TLDrawBinding } from '~types' +import type { TDShape, TldrawCommand, TDBinding } from '~types' +import type { TldrawApp } from '../../internal' export function createShapes( - data: TLDrawSnapshot, - shapes: TLDrawShape[], - bindings: TLDrawBinding[] = [] -): TLDrawCommand { - const { currentPageId } = data.appState + app: TldrawApp, + shapes: TDShape[], + bindings: TDBinding[] = [] +): TldrawCommand { + const { currentPageId } = app - const beforeShapes: Record | undefined> = {} - const afterShapes: Record | undefined> = {} + const beforeShapes: Record | undefined> = {} + const afterShapes: Record | undefined> = {} shapes.forEach((shape) => { beforeShapes[shape.id] = undefined afterShapes[shape.id] = shape }) - const beforeBindings: Record | undefined> = {} - const afterBindings: Record | undefined> = {} + const beforeBindings: Record | undefined> = {} + const afterBindings: Record | undefined> = {} bindings.forEach((binding) => { beforeBindings[binding.id] = undefined @@ -37,7 +37,7 @@ export function createShapes( }, pageStates: { [currentPageId]: { - selectedIds: [...TLDR.getSelectedIds(data, currentPageId)], + selectedIds: [...app.selectedIds], }, }, }, diff --git a/packages/tldraw/src/state/commands/deletePage/deletePage.spec.ts b/packages/tldraw/src/state/commands/deletePage/deletePage.spec.ts index 7308c1e2d..65c281dc0 100644 --- a/packages/tldraw/src/state/commands/deletePage/deletePage.spec.ts +++ b/packages/tldraw/src/state/commands/deletePage/deletePage.spec.ts @@ -1,41 +1,40 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Delete page', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when there are no pages in the current document', () => { it('does nothing', () => { - state.resetDocument() - const initialState = state.state - state.deletePage('page1') - const currentState = state.state + app.resetDocument() + const initialState = app.state + app.deletePage('page1') + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - const initialId = state.currentPageId + const initialId = app.currentPageId - state.createPage() + app.createPage() - const nextId = state.currentPageId + const nextId = app.currentPageId - state.deletePage() + app.deletePage() - expect(state.currentPageId).toBe(initialId) + expect(app.currentPageId).toBe(initialId) - state.undo() + app.undo() - expect(state.currentPageId).toBe(nextId) + expect(app.currentPageId).toBe(nextId) - state.redo() + app.redo() - expect(state.currentPageId).toBe(initialId) + expect(app.currentPageId).toBe(initialId) }) }) diff --git a/packages/tldraw/src/state/commands/deletePage/deletePage.ts b/packages/tldraw/src/state/commands/deletePage/deletePage.ts index 4f71bdf26..b0eda1eea 100644 --- a/packages/tldraw/src/state/commands/deletePage/deletePage.ts +++ b/packages/tldraw/src/state/commands/deletePage/deletePage.ts @@ -1,11 +1,13 @@ -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' +import type { TldrawApp } from '../../internal' -export function deletePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand { - const { currentPageId } = data.appState +export function deletePage(app: TldrawApp, pageId: string): TldrawCommand { + const { + currentPageId, + document: { pages, pageStates }, + } = app - const pagesArr = Object.values(data.document.pages).sort( - (a, b) => (a.childIndex || 0) - (b.childIndex || 0) - ) + const pagesArr = Object.values(pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) const currentIndex = pagesArr.findIndex((page) => page.id === pageId) @@ -29,10 +31,10 @@ export function deletePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand }, document: { pages: { - [pageId]: { ...data.document.pages[pageId] }, + [pageId]: { ...pages[pageId] }, }, pageStates: { - [pageId]: { ...data.document.pageStates[pageId] }, + [pageId]: { ...pageStates[pageId] }, }, }, }, diff --git a/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.spec.ts b/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.spec.ts index 1ec71c8bd..bfb3dfdd2 100644 --- a/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.spec.ts +++ b/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.spec.ts @@ -1,128 +1,151 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDShapeType } from '~types' describe('Delete command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.delete() - const currentState = state.state + const app = new TldrawTestApp() + const initialapp = app.state + app.delete() + const currentapp = app.state - expect(currentState).toEqual(initialState) + expect(currentapp).toEqual(initialapp) }) }) it('does, undoes and redoes command', () => { - state.select('rect2') - state.delete() + app.select('rect2') + app.delete() - expect(state.getShape('rect2')).toBe(undefined) - expect(state.getPageState().selectedIds.length).toBe(0) + expect(app.getShape('rect2')).toBe(undefined) + expect(app.selectedIds.length).toBe(0) - state.undo() + app.undo() - expect(state.getShape('rect2')).toBeTruthy() - expect(state.getPageState().selectedIds.length).toBe(1) + expect(app.getShape('rect2')).toBeTruthy() + expect(app.selectedIds.length).toBe(1) - state.redo() + app.redo() - expect(state.getShape('rect2')).toBe(undefined) - expect(state.getPageState().selectedIds.length).toBe(0) + expect(app.getShape('rect2')).toBe(undefined) + expect(app.selectedIds.length).toBe(0) }) it('deletes two shapes', () => { - state.selectAll() - state.delete() + app.selectAll() + app.delete() - expect(state.getShape('rect1')).toBe(undefined) - expect(state.getShape('rect2')).toBe(undefined) + expect(app.getShape('rect1')).toBe(undefined) + expect(app.getShape('rect2')).toBe(undefined) - state.undo() + app.undo() - expect(state.getShape('rect1')).toBeTruthy() - expect(state.getShape('rect2')).toBeTruthy() + expect(app.getShape('rect1')).toBeTruthy() + expect(app.getShape('rect2')).toBeTruthy() - state.redo() + app.redo() - expect(state.getShape('rect1')).toBe(undefined) - expect(state.getShape('rect2')).toBe(undefined) + expect(app.getShape('rect1')).toBe(undefined) + expect(app.getShape('rect2')).toBe(undefined) }) it('deletes bound shapes, undoes and redoes', () => { - new TLDrawState() + new TldrawTestApp() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } + { type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() .delete() .undo() }) it('deletes bound shapes', () => { - expect(Object.values(state.page.bindings)[0]).toBe(undefined) + expect(Object.values(app.page.bindings)[0]).toBe(undefined) - state + app .selectNone() .createShapes({ id: 'arrow1', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, }) .select('arrow1') - .startSession(SessionType.Arrow, [0, 0], 'start') - .updateSession([110, 110]) + .movePointer([0, 0]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([110, 110]) .completeSession() - const binding = Object.values(state.page.bindings)[0] + const binding = Object.values(app.page.bindings)[0] expect(binding).toBeTruthy() expect(binding.fromId).toBe('arrow1') expect(binding.toId).toBe('rect3') expect(binding.handleId).toBe('start') - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) - state.select('rect3').delete() + app.select('rect3').delete() - expect(Object.values(state.page.bindings)[0]).toBe(undefined) - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined) + expect(Object.values(app.page.bindings)[0]).toBe(undefined) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(undefined) - state.undo() + app.undo() - expect(Object.values(state.page.bindings)[0]).toBeTruthy() - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) + expect(Object.values(app.page.bindings)[0]).toBeTruthy() + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) - state.redo() + app.redo() - expect(Object.values(state.page.bindings)[0]).toBe(undefined) - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined) + expect(Object.values(app.page.bindings)[0]).toBe(undefined) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(undefined) }) describe('when deleting shapes in a group', () => { it('updates the group', () => { - state.group(['rect1', 'rect2', 'rect3'], 'newGroup').select('rect1').delete() + app.group(['rect1', 'rect2', 'rect3'], 'newGroup').select('rect1').delete() - expect(state.getShape('rect1')).toBeUndefined() - expect(state.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3']) + expect(app.getShape('rect1')).toBeUndefined() + expect(app.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3']) }) }) describe('when deleting a group', () => { it('deletes all grouped shapes', () => { - state.group(['rect1', 'rect2'], 'newGroup').select('newGroup').delete() + app.group(['rect1', 'rect2'], 'newGroup').select('newGroup').delete() - expect(state.getShape('rect1')).toBeUndefined() - expect(state.getShape('rect2')).toBeUndefined() - expect(state.getShape('newGroup')).toBeUndefined() + expect(app.getShape('rect1')).toBeUndefined() + expect(app.getShape('rect2')).toBeUndefined() + expect(app.getShape('newGroup')).toBeUndefined() + + app.undo() + expect(app.getShape('rect1')).toBeTruthy() + expect(app.getShape('rect2')).toBeTruthy() + expect(app.getShape('newGroup')).toBeTruthy() + }) + }) + + describe('when deleting grouped shapes', () => { + it('deletes the group too', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2', 'rect3'], 'newGroup') + .select('rect1', 'rect2', 'rect3') + .delete() + + expect(app.getShape('newGroup')).toBeUndefined() + + app.undo() + + expect(app.getShape('newGroup')).toBeTruthy() }) }) diff --git a/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.ts b/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.ts index 0cd0c0c41..0ff2b8326 100644 --- a/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.ts +++ b/packages/tldraw/src/state/commands/deleteShapes/deleteShapes.ts @@ -1,13 +1,15 @@ +import type { TldrawApp } from '../../internal' import { TLDR } from '~state/TLDR' -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' import { removeShapesFromPage } from '../shared/removeShapesFromPage' export function deleteShapes( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], - pageId = data.appState.currentPageId -): TLDrawCommand { - const { before, after } = removeShapesFromPage(data, ids, pageId) + pageId = app.currentPageId +): TldrawCommand { + const { pageState, selectedIds } = app + const { before, after } = removeShapesFromPage(app.state, ids, pageId) return { id: 'delete', @@ -17,7 +19,7 @@ export function deleteShapes( [pageId]: before, }, pageStates: { - [pageId]: { selectedIds: TLDR.getSelectedIds(data, pageId) }, + [pageId]: { selectedIds: [...app.selectedIds] }, }, }, }, @@ -27,7 +29,13 @@ export function deleteShapes( [pageId]: after, }, pageStates: { - [pageId]: { selectedIds: [] }, + [pageId]: { + selectedIds: selectedIds.filter((id) => !ids.includes(id)), + hoveredId: + pageState.hoveredId && ids.includes(pageState.hoveredId) + ? undefined + : pageState.hoveredId, + }, }, }, }, diff --git a/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.spec.ts b/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.spec.ts index 76094b848..b0c559918 100644 --- a/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.spec.ts +++ b/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.spec.ts @@ -1,68 +1,70 @@ import Vec from '@tldraw/vec' -import { TLDrawState } from '~state' -import { mockDocument, TLDrawStateUtils } from '~test' -import { AlignType, DistributeType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { DistributeType, TDShapeType } from '~types' describe('Distribute command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when less than three shapes are selected', () => { it('does nothing', () => { - state.select('rect1', 'rect2') - const initialState = state.state - state.distribute(DistributeType.Horizontal) - const currentState = state.state + app.select('rect1', 'rect2') + const initialState = app.state + app.distribute(DistributeType.Horizontal) + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.selectAll() - state.distribute(DistributeType.Horizontal) + app.selectAll() + app.distribute(DistributeType.Horizontal) - expect(state.getShape('rect3').point).toEqual([50, 20]) - state.undo() - expect(state.getShape('rect3').point).toEqual([20, 20]) - state.redo() - expect(state.getShape('rect3').point).toEqual([50, 20]) + expect(app.getShape('rect3').point).toEqual([50, 20]) + app.undo() + expect(app.getShape('rect3').point).toEqual([20, 20]) + app.redo() + expect(app.getShape('rect3').point).toEqual([50, 20]) }) it('distributes vertically', () => { - state.selectAll() - state.distribute(DistributeType.Vertical) + app.selectAll() + app.distribute(DistributeType.Vertical) - expect(state.getShape('rect3').point).toEqual([20, 50]) + expect(app.getShape('rect3').point).toEqual([20, 50]) }) }) describe('when distributing groups', () => { it('distributes children', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( - { id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] }, - { id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] }, - { id: 'rect3', type: TLDrawShapeType.Rectangle, point: [200, 200], size: [100, 100] }, - { id: 'rect4', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [200, 200] }, - { id: 'rect5', type: TLDrawShapeType.Rectangle, point: [300, -200], size: [100, 100] } + { id: 'rect1', type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100] }, + { id: 'rect2', type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100] }, + { id: 'rect3', type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100] }, + { id: 'rect4', type: TDShapeType.Rectangle, point: [0, 0], size: [200, 200] }, + { id: 'rect5', type: TDShapeType.Rectangle, point: [300, -200], size: [100, 100] } ) .group(['rect1', 'rect2'], 'groupA') .select('rect3', 'rect4', 'rect5') .distribute(DistributeType.Vertical) - const p0 = state.getShape('rect4').point - const p1 = state.getShape('rect3').point + const p0 = app.getShape('rect4').point + const p1 = app.getShape('rect3').point - state.undo().delete(['rect4']).selectAll().distribute(DistributeType.Vertical) - - new TLDrawStateUtils(state).expectShapesToBeAtPoints({ - rect1: p0, - rect2: Vec.add(p0, [100, 100]), - rect3: p1, - }) + app + .undo() + .delete(['rect4']) + .selectAll() + .distribute(DistributeType.Vertical) + .expectShapesToBeAtPoints({ + rect1: p0, + rect2: Vec.add(p0, [100, 100]), + rect3: p1, + }) }) }) diff --git a/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.ts b/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.ts index f5e66cfd4..7c5d6f0d8 100644 --- a/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.ts +++ b/packages/tldraw/src/state/commands/distributeShapes/distributeShapes.ts @@ -1,36 +1,34 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Utils } from '@tldraw/core' -import { DistributeType, TLDrawShape, TLDrawSnapshot, TLDrawCommand, TLDrawShapeType } from '~types' +import { DistributeType, TDShape, TldrawCommand, TDShapeType } from '~types' import { TLDR } from '~state/TLDR' import Vec from '@tldraw/vec' +import type { TldrawApp } from '../../internal' export function distributeShapes( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], type: DistributeType -): TLDrawCommand { - const { currentPageId } = data.appState +): TldrawCommand { + const { currentPageId } = app - const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId)) + const initialShapes = ids.map((id) => app.getShape(id)) const deltaMap = Object.fromEntries(getDistributions(initialShapes, type).map((d) => [d.id, d])) const { before, after } = TLDR.mutateShapes( - data, - ids, - (shape) => { - if (!deltaMap[shape.id]) return shape - return { point: deltaMap[shape.id].next } - }, + app.state, + ids.filter((id) => deltaMap[id] !== undefined), + (shape) => ({ point: deltaMap[shape.id].next }), currentPageId ) initialShapes.forEach((shape) => { - if (shape.type === TLDrawShapeType.Group) { + if (shape.type === TDShapeType.Group) { const delta = Vec.sub(after[shape.id].point!, before[shape.id].point!) shape.children.forEach((id) => { - const child = TLDR.getShape(data, id, currentPageId) + const child = app.getShape(id) before[child.id] = { point: child.point } after[child.id] = { point: Vec.add(child.point, delta) } }) @@ -69,9 +67,9 @@ export function distributeShapes( } } -function getDistributions(initialShapes: TLDrawShape[], type: DistributeType) { +function getDistributions(initialShapes: TDShape[], type: DistributeType) { const entries = initialShapes.map((shape) => { - const utils = TLDR.getShapeUtils(shape) + const utils = TLDR.getShapeUtil(shape) return { id: shape.id, point: [...shape.point], diff --git a/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.spec.ts b/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.spec.ts index a66091057..7192cb587 100644 --- a/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.spec.ts +++ b/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.spec.ts @@ -1,24 +1,23 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Duplicate page command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() it('does, undoes and redoes command', () => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) - const initialId = state.page.id + const initialId = app.page.id - state.duplicatePage(state.currentPageId) + app.duplicatePage(app.currentPageId) - const nextId = state.page.id + const nextId = app.page.id - state.undo() + app.undo() - expect(state.page.id).toBe(initialId) + expect(app.page.id).toBe(initialId) - state.redo() + app.redo() - expect(state.page.id).toBe(nextId) + expect(app.page.id).toBe(nextId) }) }) diff --git a/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.ts b/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.ts index b0bdc7952..a45e72fc1 100644 --- a/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.ts +++ b/packages/tldraw/src/state/commands/duplicatePage/duplicatePage.ts @@ -1,15 +1,14 @@ -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' import { Utils } from '@tldraw/core' +import type { TldrawApp } from '../../internal' -export function duplicatePage( - data: TLDrawSnapshot, - center: number[], - pageId: string -): TLDrawCommand { +export function duplicatePage(app: TldrawApp, pageId: string): TldrawCommand { const newId = Utils.uniqueId() - const { currentPageId } = data.appState - - const page = data.document.pages[pageId] + const { + currentPageId, + page, + pageState: { camera }, + } = app const nextPage = { ...page, @@ -56,7 +55,7 @@ export function duplicatePage( ...page, id: newId, selectedIds: [], - camera: data.document.pageStates[currentPageId].camera, + camera: { ...camera }, editingId: undefined, bindingId: undefined, hoveredId: undefined, diff --git a/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.spec.ts b/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.spec.ts index 84584dbda..7d142d299 100644 --- a/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.spec.ts +++ b/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.spec.ts @@ -1,43 +1,42 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Utils } from '@tldraw/core' -import { TLDrawState } from '~state' import { TLDR } from '~state/TLDR' -import { mockDocument } from '~test' -import { ArrowShape, SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, SessionType, TDShapeType } from '~types' describe('Duplicate command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.duplicate() - const currentState = state.state + const initialState = app.state + app.duplicate() + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.select('rect1') + app.select('rect1') - expect(Object.keys(state.getPage().shapes).length).toBe(3) + expect(Object.keys(app.getPage().shapes).length).toBe(3) - state.duplicate() + app.duplicate() - expect(Object.keys(state.getPage().shapes).length).toBe(4) + expect(Object.keys(app.getPage().shapes).length).toBe(4) - state.undo() + app.undo() - expect(Object.keys(state.getPage().shapes).length).toBe(3) + expect(Object.keys(app.getPage().shapes).length).toBe(3) - state.redo() + app.redo() - expect(Object.keys(state.getPage().shapes).length).toBe(4) + expect(Object.keys(app.getPage().shapes).length).toBe(4) }) describe('when duplicating a shape', () => { @@ -46,128 +45,130 @@ describe('Duplicate command', () => { describe('when duplicating a bound shape', () => { it('removed the binding when the target is not selected', () => { - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'target1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], }, { - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200], } ) - const beforeShapeIds = Object.keys(state.page.shapes) + const beforeShapeIds = Object.keys(app.page.shapes) - state + app .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const beforeArrow = state.getShape('arrow1') + const beforeArrow = app.getShape('arrow1') expect(beforeArrow.handles.start.bindingId).toBeTruthy() - state.select('arrow1').duplicate() + app.select('arrow1').duplicate() - const afterShapeIds = Object.keys(state.page.shapes) + const afterShapeIds = Object.keys(app.page.shapes) const newShapeIds = afterShapeIds.filter((id) => !beforeShapeIds.includes(id)) expect(newShapeIds.length).toBe(1) - const duplicatedArrow = state.getShape(newShapeIds[0]) + const duplicatedArrow = app.getShape(newShapeIds[0]) expect(duplicatedArrow.handles.start.bindingId).toBeUndefined() }) it('duplicates the binding when the target is selected', () => { - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'target1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], }, { - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200], } ) - const beforeShapeIds = Object.keys(state.page.shapes) + const beforeShapeIds = Object.keys(app.page.shapes) - state + app .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const oldBindingId = state.getShape('arrow1').handles.start.bindingId + const oldBindingId = app.getShape('arrow1').handles.start.bindingId expect(oldBindingId).toBeTruthy() - state.select('arrow1', 'target1').duplicate() + app.select('arrow1', 'target1').duplicate() - const afterShapeIds = Object.keys(state.page.shapes) + const afterShapeIds = Object.keys(app.page.shapes) const newShapeIds = afterShapeIds.filter((id) => !beforeShapeIds.includes(id)) expect(newShapeIds.length).toBe(2) - const newBindingId = state.getShape(newShapeIds[0]).handles.start.bindingId + const newBindingId = app.getShape(newShapeIds[0]).handles.start.bindingId expect(newBindingId).toBeTruthy() - state.undo() + app.undo() - expect(state.getBinding(newBindingId!)).toBeUndefined() - expect(state.getShape(newShapeIds[0])).toBeUndefined() + expect(app.getBinding(newBindingId!)).toBeUndefined() + expect(app.getShape(newShapeIds[0])).toBeUndefined() - state.redo() + app.redo() - expect(state.getBinding(newBindingId!)).toBeTruthy() - expect(state.getShape(newShapeIds[0]).handles.start.bindingId).toBe(newBindingId) + expect(app.getBinding(newBindingId!)).toBeTruthy() + expect(app.getShape(newShapeIds[0]).handles.start.bindingId).toBe(newBindingId) }) it('duplicates groups', () => { - state.group(['rect1', 'rect2'], 'newGroup').select('newGroup') + app.group(['rect1', 'rect2'], 'newGroup').select('newGroup') - const beforeShapeIds = Object.keys(state.page.shapes) + const beforeShapeIds = Object.keys(app.page.shapes) - state.duplicate() + app.duplicate() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 3) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length + 3) - state.undo() + app.undo() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length) - state.redo() + app.redo() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 3) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length + 3) }) it('duplicates grouped shapes', () => { - state.group(['rect1', 'rect2'], 'newGroup').select('rect1') + app.group(['rect1', 'rect2'], 'newGroup').select('rect1') - const beforeShapeIds = Object.keys(state.page.shapes) + const beforeShapeIds = Object.keys(app.page.shapes) - state.duplicate() + app.duplicate() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 1) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length + 1) - state.undo() + app.undo() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length) - state.redo() + app.redo() - expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 1) + expect(Object.keys(app.page.shapes).length).toBe(beforeShapeIds.length + 1) }) }) @@ -176,25 +177,25 @@ describe('Duplicate command', () => { describe('when point-duplicating', () => { it('duplicates without crashing', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() - state + app .loadDocument(mockDocument) .group(['rect1', 'rect2']) .selectAll() - .duplicate(state.selectedIds, [200, 200]) + .duplicate(app.selectedIds, [200, 200]) }) it('duplicates in the correct place', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() - state.loadDocument(mockDocument).group(['rect1', 'rect2']).selectAll() + app.loadDocument(mockDocument).group(['rect1', 'rect2']).selectAll() - const before = state.shapes.map((shape) => shape.id) + const before = app.shapes.map((shape) => shape.id) - state.duplicate(state.selectedIds, [200, 200]) + app.duplicate(app.selectedIds, [200, 200]) - const after = state.shapes.filter((shape) => !before.includes(shape.id)) + const after = app.shapes.filter((shape) => !before.includes(shape.id)) expect( Utils.getBoundsCenter(Utils.getCommonBounds(after.map((shape) => TLDR.getBounds(shape)))) diff --git a/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.ts b/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.ts index 97aa1b7b0..bc96926f6 100644 --- a/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.ts +++ b/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.ts @@ -2,16 +2,11 @@ import { Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' import { TLDR } from '~state/TLDR' -import type { TLDrawSnapshot, PagePartial, TLDrawCommand, TLDrawShape } from '~types' +import type { PagePartial, TldrawCommand, TDShape } from '~types' +import type { TldrawApp } from '../../internal' -export function duplicateShapes( - data: TLDrawSnapshot, - ids: string[], - point?: number[] -): TLDrawCommand { - const { currentPageId } = data.appState - - const page = TLDR.getPage(data, currentPageId) +export function duplicateShapes(app: TldrawApp, ids: string[], point?: number[]): TldrawCommand { + const { selectedIds, currentPageId, page, shapes } = app const before: PagePartial = { shapes: {}, @@ -23,51 +18,49 @@ export function duplicateShapes( bindings: {}, } - const shapes = TLDR.getSelectedIds(data, currentPageId).map((id) => - TLDR.getShape(data, id, currentPageId) - ) - const duplicateMap: Record = {} - // Create duplicates - shapes + const shapesToDuplicate = ids + .map((id) => app.getShape(id)) .filter((shape) => !ids.includes(shape.parentId)) - .forEach((shape) => { - const duplicatedId = Utils.uniqueId() - before.shapes[duplicatedId] = undefined - after.shapes[duplicatedId] = { - ...Utils.deepClone(shape), - id: duplicatedId, - childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId), + // Create duplicates + shapesToDuplicate.forEach((shape) => { + const duplicatedId = Utils.uniqueId() + before.shapes[duplicatedId] = undefined + + after.shapes[duplicatedId] = { + ...Utils.deepClone(shape), + id: duplicatedId, + childIndex: TLDR.getChildIndexAbove(app.state, shape.id, currentPageId), + } + + if (shape.children) { + after.shapes[duplicatedId]!.children = [] + } + + if (shape.parentId !== currentPageId) { + const parent = app.getShape(shape.parentId) + + before.shapes[parent.id] = { + ...before.shapes[parent.id], + children: parent.children, } - if (shape.children) { - after.shapes[duplicatedId]!.children = [] + after.shapes[parent.id] = { + ...after.shapes[parent.id], + children: [...(after.shapes[parent.id] || parent).children!, duplicatedId], } + } - if (shape.parentId !== currentPageId) { - const parent = TLDR.getShape(data, shape.parentId, currentPageId) - - before.shapes[parent.id] = { - ...before.shapes[parent.id], - children: parent.children, - } - - after.shapes[parent.id] = { - ...after.shapes[parent.id], - children: [...(after.shapes[parent.id] || parent).children!, duplicatedId], - } - } - - duplicateMap[shape.id] = duplicatedId - }) + duplicateMap[shape.id] = duplicatedId + }) // If the shapes have children, then duplicate those too - shapes.forEach((shape) => { + shapesToDuplicate.forEach((shape) => { if (shape.children) { shape.children.forEach((childId) => { - const child = TLDR.getShape(data, childId, currentPageId) + const child = app.getShape(childId) const duplicatedId = Utils.uniqueId() const duplicatedParentId = duplicateMap[shape.id] before.shapes[duplicatedId] = undefined @@ -75,7 +68,7 @@ export function duplicateShapes( ...Utils.deepClone(child), id: duplicatedId, parentId: duplicatedParentId, - childIndex: TLDR.getChildIndexAbove(data, child.id, currentPageId), + childIndex: TLDR.getChildIndexAbove(app.state, child.id, currentPageId), } duplicateMap[childId] = duplicatedId after.shapes[duplicateMap[shape.id]]?.children?.push(duplicatedId) @@ -129,7 +122,7 @@ export function duplicateShapes( // Now move the shapes - const shapesToMove = Object.values(after.shapes) as TLDrawShape[] + const shapesToMove = Object.values(after.shapes) as TDShape[] if (point) { const commonBounds = Utils.getCommonBounds(shapesToMove.map((shape) => TLDR.getBounds(shape))) @@ -148,6 +141,13 @@ export function duplicateShapes( }) } + // Unlock any locked shapes + shapesToMove.forEach((shape) => { + if (shape.isLocked) { + shape.isLocked = false + } + }) + return { id: 'duplicate', before: { @@ -156,7 +156,7 @@ export function duplicateShapes( [currentPageId]: before, }, pageStates: { - [currentPageId]: { selectedIds: ids }, + [currentPageId]: { selectedIds }, }, }, }, diff --git a/packages/tldraw/src/state/commands/flipShapes/flipShapes.spec.ts b/packages/tldraw/src/state/commands/flipShapes/flipShapes.spec.ts index db66f29cc..db38de9e6 100644 --- a/packages/tldraw/src/state/commands/flipShapes/flipShapes.spec.ts +++ b/packages/tldraw/src/state/commands/flipShapes/flipShapes.spec.ts @@ -1,50 +1,49 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' import type { RectangleShape } from '~types' describe('Flip command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.flipHorizontal() - const currentState = state.state + const initialState = app.state + app.flipHorizontal() + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.select('rect1', 'rect2') - state.flipHorizontal() + app.select('rect1', 'rect2') + app.flipHorizontal() - expect(state.getShape('rect1').point).toStrictEqual([100, 0]) + expect(app.getShape('rect1').point).toStrictEqual([100, 0]) - state.undo() + app.undo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) - state.redo() + app.redo() - expect(state.getShape('rect1').point).toStrictEqual([100, 0]) + expect(app.getShape('rect1').point).toStrictEqual([100, 0]) }) it('flips horizontally', () => { - state.select('rect1', 'rect2') - state.flipHorizontal() + app.select('rect1', 'rect2') + app.flipHorizontal() - expect(state.getShape('rect1').point).toStrictEqual([100, 0]) + expect(app.getShape('rect1').point).toStrictEqual([100, 0]) }) it('flips vertically', () => { - state.select('rect1', 'rect2') - state.flipVertical() + app.select('rect1', 'rect2') + app.flipVertical() - expect(state.getShape('rect1').point).toStrictEqual([0, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 100]) }) }) diff --git a/packages/tldraw/src/state/commands/flipShapes/flipShapes.ts b/packages/tldraw/src/state/commands/flipShapes/flipShapes.ts index ec4046f63..56f9150b8 100644 --- a/packages/tldraw/src/state/commands/flipShapes/flipShapes.ts +++ b/packages/tldraw/src/state/commands/flipShapes/flipShapes.ts @@ -1,18 +1,18 @@ import { FlipType } from '~types' import { TLBoundsCorner, Utils } from '@tldraw/core' -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' +import type { TldrawApp } from '../../internal' import { TLDR } from '~state/TLDR' -export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): TLDrawCommand { - const { currentPageId } = data.appState - const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId)) +export function flipShapes(app: TldrawApp, ids: string[], type: FlipType): TldrawCommand { + const { selectedIds, currentPageId, shapes } = app - const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) + const boundsForShapes = shapes.map((shape) => TLDR.getBounds(shape)) const commonBounds = Utils.getCommonBounds(boundsForShapes) const { before, after } = TLDR.mutateShapes( - data, + app.state, ids, (shape) => { const shapeBounds = TLDR.getBounds(shape) @@ -27,7 +27,7 @@ export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): false ) - return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, { + return TLDR.getShapeUtil(shape).transform(shape, newShapeBounds, { type: TLBoundsCorner.TopLeft, scaleX: -1, scaleY: 1, @@ -44,7 +44,7 @@ export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): true ) - return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, { + return TLDR.getShapeUtil(shape).transform(shape, newShapeBounds, { type: TLBoundsCorner.TopLeft, scaleX: 1, scaleY: -1, @@ -62,11 +62,11 @@ export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): before: { document: { pages: { - [data.appState.currentPageId]: { shapes: before }, + [currentPageId]: { shapes: before }, }, pageStates: { [currentPageId]: { - selectedIds: ids, + selectedIds, }, }, }, @@ -74,7 +74,7 @@ export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): after: { document: { pages: { - [data.appState.currentPageId]: { shapes: after }, + [currentPageId]: { shapes: after }, }, pageStates: { [currentPageId]: { diff --git a/packages/tldraw/src/state/commands/groupShapes/groupShapes.spec.ts b/packages/tldraw/src/state/commands/groupShapes/groupShapes.spec.ts index 108f628d2..c4d0c7660 100644 --- a/packages/tldraw/src/state/commands/groupShapes/groupShapes.spec.ts +++ b/packages/tldraw/src/state/commands/groupShapes/groupShapes.spec.ts @@ -1,45 +1,44 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { GroupShape, TLDrawShape, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { GroupShape, TDShape, TDShapeType } from '~types' describe('Group command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) it('does, undoes and redoes command', () => { - state.group(['rect1', 'rect2'], 'newGroup') + app.group(['rect1', 'rect2'], 'newGroup') - expect(state.getShape('newGroup')).toBeTruthy() + expect(app.getShape('newGroup')).toBeTruthy() - state.undo() + app.undo() - expect(state.getShape('newGroup')).toBeUndefined() + expect(app.getShape('newGroup')).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('newGroup')).toBeTruthy() + expect(app.getShape('newGroup')).toBeTruthy() }) describe('when less than two shapes are selected', () => { it('does nothing', () => { - state.selectNone() + app.selectNone() // @ts-ignore - const stackLength = state.stack.length + const stackLength = app.stack.length - state.group([], 'newGroup') - expect(state.getShape('newGroup')).toBeUndefined() + app.group([], 'newGroup') + expect(app.getShape('newGroup')).toBeUndefined() // @ts-ignore - expect(state.stack.length).toBe(stackLength) + expect(app.stack.length).toBe(stackLength) - state.group(['rect1'], 'newGroup') - expect(state.getShape('newGroup')).toBeUndefined() + app.group(['rect1'], 'newGroup') + expect(app.getShape('newGroup')).toBeUndefined() // @ts-ignore - expect(state.stack.length).toBe(stackLength) + expect(app.stack.length).toBe(stackLength) }) }) @@ -51,7 +50,7 @@ describe('Group command', () => { */ it('creates a group with the correct props', () => { - state.updateShapes( + app.updateShapes( { id: 'rect1', point: [300, 300], @@ -64,8 +63,8 @@ describe('Group command', () => { } ) - state.group(['rect1', 'rect2'], 'newGroup') - const group = state.getShape('newGroup') + app.group(['rect1', 'rect2'], 'newGroup') + const group = app.getShape('newGroup') expect(group).toBeTruthy() expect(group.parentId).toBe('page1') expect(group.childIndex).toBe(3) @@ -74,7 +73,7 @@ describe('Group command', () => { }) it('reparents the grouped shapes', () => { - state.updateShapes( + app.updateShapes( { id: 'rect1', childIndex: 2.5, @@ -85,13 +84,13 @@ describe('Group command', () => { } ) - state.group(['rect1', 'rect2'], 'newGroup') + app.group(['rect1', 'rect2'], 'newGroup') - let rect1: TLDrawShape - let rect2: TLDrawShape + let rect1: TDShape + let rect2: TDShape - rect1 = state.getShape('rect1') - rect2 = state.getShape('rect2') + rect1 = app.getShape('rect1') + rect2 = app.getShape('rect2') // Reparents the shapes expect(rect1.parentId).toBe('newGroup') expect(rect2.parentId).toBe('newGroup') @@ -99,10 +98,10 @@ describe('Group command', () => { expect(rect1.childIndex).toBe(1) expect(rect2.childIndex).toBe(2) - state.undo() + app.undo() - rect1 = state.getShape('rect1') - rect2 = state.getShape('rect2') + rect1 = app.getShape('rect1') + rect2 = app.getShape('rect2') // Restores the shapes' parentIds expect(rect1.parentId).toBe('page1') expect(rect2.parentId).toBe('page1') @@ -127,82 +126,82 @@ describe('Group command', () => { original group be updated to only contain the remaining ones. */ - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, }, { id: 'rect4', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 4, } ) - state.group(['rect1', 'rect2', 'rect3', 'rect4'], 'newGroupA') + app.group(['rect1', 'rect2', 'rect3', 'rect4'], 'newGroupA') - expect(state.getShape('newGroupA')).toBeTruthy() - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect3').childIndex).toBe(3) - expect(state.getShape('rect4').childIndex).toBe(4) - expect(state.getShape('newGroupA').children).toStrictEqual([ + expect(app.getShape('newGroupA')).toBeTruthy() + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect3').childIndex).toBe(3) + expect(app.getShape('rect4').childIndex).toBe(4) + expect(app.getShape('newGroupA').children).toStrictEqual([ 'rect1', 'rect2', 'rect3', 'rect4', ]) - state.group(['rect1', 'rect3'], 'newGroupB') + app.group(['rect1', 'rect3'], 'newGroupB') - expect(state.getShape('newGroupA')).toBeTruthy() - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect4').childIndex).toBe(4) - expect(state.getShape('newGroupA').children).toStrictEqual(['rect2', 'rect4']) + expect(app.getShape('newGroupA')).toBeTruthy() + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect4').childIndex).toBe(4) + expect(app.getShape('newGroupA').children).toStrictEqual(['rect2', 'rect4']) - expect(state.getShape('newGroupB')).toBeTruthy() - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect3').childIndex).toBe(2) - expect(state.getShape('newGroupB').children).toStrictEqual(['rect1', 'rect3']) + expect(app.getShape('newGroupB')).toBeTruthy() + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect3').childIndex).toBe(2) + expect(app.getShape('newGroupB').children).toStrictEqual(['rect1', 'rect3']) - state.undo() + app.undo() - expect(state.getShape('newGroupA')).toBeTruthy() - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect3').childIndex).toBe(3) - expect(state.getShape('rect4').childIndex).toBe(4) - expect(state.getShape('newGroupA').children).toStrictEqual([ + expect(app.getShape('newGroupA')).toBeTruthy() + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect3').childIndex).toBe(3) + expect(app.getShape('rect4').childIndex).toBe(4) + expect(app.getShape('newGroupA').children).toStrictEqual([ 'rect1', 'rect2', 'rect3', 'rect4', ]) - expect(state.getShape('newGroupB')).toBeUndefined() + expect(app.getShape('newGroupB')).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('newGroupA')).toBeTruthy() - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect4').childIndex).toBe(4) - expect(state.getShape('newGroupA').children).toStrictEqual(['rect2', 'rect4']) + expect(app.getShape('newGroupA')).toBeTruthy() + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect4').childIndex).toBe(4) + expect(app.getShape('newGroupA').children).toStrictEqual(['rect2', 'rect4']) - expect(state.getShape('newGroupB')).toBeTruthy() - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect3').childIndex).toBe(2) - expect(state.getShape('newGroupB').children).toStrictEqual(['rect1', 'rect3']) + expect(app.getShape('newGroupB')).toBeTruthy() + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect3').childIndex).toBe(2) + expect(app.getShape('newGroupB').children).toStrictEqual(['rect1', 'rect3']) }) it('does nothing if all shapes in the group are selected', () => { @@ -210,27 +209,27 @@ describe('Group command', () => { If the selected shapes represent ALL of the children of the a group, then no effect should occur. */ - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) - state.group(['rect1', 'rect2', 'rect3'], 'newGroupA') - state.group(['rect1', 'rect2', 'rect3'], 'newGroupB') - expect(state.getShape('newGroupB')).toBeUndefined() + app.group(['rect1', 'rect2', 'rect3'], 'newGroupA') + app.group(['rect1', 'rect2', 'rect3'], 'newGroupB') + expect(app.getShape('newGroupB')).toBeUndefined() }) it('deletes any groups that no longer have children', () => { @@ -240,28 +239,28 @@ describe('Group command', () => { Other rules around deleted shapes should here apply: bindings connected to the group should be deleted, etc. */ - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) - state.group(['rect1', 'rect2'], 'newGroupA') - state.group(['rect1', 'rect2', 'rect3'], 'newGroupB') - expect(state.getShape('newGroupA')).toBeUndefined() - expect(state.getShape('newGroupB').children).toStrictEqual([ + app.group(['rect1', 'rect2'], 'newGroupA') + app.group(['rect1', 'rect2', 'rect3'], 'newGroupB') + expect(app.getShape('newGroupA')).toBeUndefined() + expect(app.getShape('newGroupB').children).toStrictEqual([ 'rect1', 'rect2', 'rect3', @@ -274,44 +273,44 @@ describe('Group command', () => { groups, then the selected groups should be destroyed and a new group created with the selected shapes and the group(s)' children. */ - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) - state.group(['rect1', 'rect2'], 'newGroupA') - state.group(['newGroupA', 'rect3'], 'newGroupB') - expect(state.getShape('newGroupA')).toBeUndefined() - expect(state.getShape('newGroupB').children).toStrictEqual([ + app.group(['rect1', 'rect2'], 'newGroupA') + app.group(['newGroupA', 'rect3'], 'newGroupB') + expect(app.getShape('newGroupA')).toBeUndefined() + expect(app.getShape('newGroupB').children).toStrictEqual([ 'rect1', 'rect2', 'rect3', ]) - state.undo() + app.undo() - expect(state.getShape('newGroupB')).toBeUndefined() - expect(state.getShape('newGroupA')).toBeDefined() - expect(state.getShape('newGroupA').children).toStrictEqual(['rect1', 'rect2']) + expect(app.getShape('newGroupB')).toBeUndefined() + expect(app.getShape('newGroupA')).toBeDefined() + expect(app.getShape('newGroupA').children).toStrictEqual(['rect1', 'rect2']) - state.redo() + app.redo() - expect(state.getShape('newGroupA')).toBeUndefined() - expect(state.getShape('newGroupB')).toBeDefined() - expect(state.getShape('newGroupB').children).toStrictEqual([ + expect(app.getShape('newGroupA')).toBeUndefined() + expect(app.getShape('newGroupB')).toBeDefined() + expect(app.getShape('newGroupB').children).toStrictEqual([ 'rect1', 'rect2', 'rect3', @@ -319,33 +318,33 @@ describe('Group command', () => { }) it('Ungroups if the only shape selected is a group', () => { - state.resetDocument().createShapes( + app.resetDocument().createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) - expect(state.shapes.length).toBe(3) + expect(app.shapes.length).toBe(3) - state.selectAll().group() + app.selectAll().group() - expect(state.shapes.length).toBe(4) + expect(app.shapes.length).toBe(4) - state.selectAll().group() + app.selectAll().group() - expect(state.shapes.length).toBe(3) + expect(app.shapes.length).toBe(3) }) /* diff --git a/packages/tldraw/src/state/commands/groupShapes/groupShapes.ts b/packages/tldraw/src/state/commands/groupShapes/groupShapes.ts index 1f873921e..b6c5a0685 100644 --- a/packages/tldraw/src/state/commands/groupShapes/groupShapes.ts +++ b/packages/tldraw/src/state/commands/groupShapes/groupShapes.ts @@ -1,35 +1,40 @@ -import { TLDrawBinding, TLDrawShape, TLDrawShapeType } from '~types' +import { TDShape, TDShapeType } from '~types' import { Utils } from '@tldraw/core' -import type { TLDrawSnapshot, TLDrawCommand } from '~types' -import { TLDR } from '~state/TLDR' +import type { TDSnapshot, TldrawCommand, TDBinding } from '~types' import type { Patch } from 'rko' +import type { TldrawApp } from '../../internal' +import { TLDR } from '~state/TLDR' export function groupShapes( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], groupId: string, pageId: string -): TLDrawCommand | undefined { - const beforeShapes: Record> = {} - const afterShapes: Record> = {} +): TldrawCommand | undefined { + const beforeShapes: Record> = {} + const afterShapes: Record> = {} - const beforeBindings: Record> = {} - const afterBindings: Record> = {} + const beforeBindings: Record> = {} + const afterBindings: Record> = {} const idsToGroup = [...ids] - const shapesToGroup: TLDrawShape[] = [] + const shapesToGroup: TDShape[] = [] const deletedGroupIds: string[] = [] - const otherEffectedGroups: TLDrawShape[] = [] + const otherEffectedGroups: TDShape[] = [] // Collect all of the shapes to group (and their ids) for (const id of ids) { - const shape = TLDR.getShape(data, id, pageId) + const shape = app.getShape(id) + if (shape.isLocked) continue + if (shape.children === undefined) { shapesToGroup.push(shape) } else { + const childIds = shape.children.filter((id) => !app.getShape(id).isLocked) + otherEffectedGroups.push(shape) - idsToGroup.push(...shape.children) - shapesToGroup.push(...shape.children.map((id) => TLDR.getShape(data, id, pageId))) + idsToGroup.push(...childIds) + shapesToGroup.push(...childIds.map((id) => app.getShape(id))) } } @@ -39,7 +44,7 @@ export function groupShapes( if (shapesToGroup.every((shape) => shape.parentId === shapesToGroup[0].parentId)) { // Is the common parent a shape (not the page)? if (shapesToGroup[0].parentId !== pageId) { - const commonParent = TLDR.getShape(data, shapesToGroup[0].parentId, pageId) + const commonParent = app.getShape(shapesToGroup[0].parentId) // Are all of the common parent's shapes selected? if (commonParent.children?.length === idsToGroup.length) { // Don't create a group if that group would be the same as the @@ -50,7 +55,7 @@ export function groupShapes( } // A flattened array of shapes from the page - const flattenedShapes = TLDR.flattenPage(data, pageId) + const flattenedShapes = TLDR.flattenPage(app.state, pageId) // A map of shapes to their index in flattendShapes const shapeIndexMap = Object.fromEntries( @@ -76,7 +81,7 @@ export function groupShapes( // Create the group beforeShapes[groupId] = undefined - afterShapes[groupId] = TLDR.getShapeUtils(TLDrawShapeType.Group).create({ + afterShapes[groupId] = TLDR.getShapeUtil(TDShapeType.Group).create({ id: groupId, childIndex: groupChildIndex, parentId: groupParentId, @@ -89,7 +94,7 @@ export function groupShapes( sortedShapes.forEach((shape, index) => { // If the shape is part of a different group, mark the parent shape for cleanup if (shape.parentId !== pageId) { - const parentShape = TLDR.getShape(data, shape.parentId, pageId) + const parentShape = app.getShape(shape.parentId) otherEffectedGroups.push(parentShape) } @@ -125,7 +130,7 @@ export function groupShapes( // (This is necessary only when we implement nested groups.) if (shape.parentId !== pageId) { deletedGroupIds.push(shape.id) - otherEffectedGroups.push(TLDR.getShape(data, shape.parentId, pageId)) + otherEffectedGroups.push(app.getShape(shape.parentId)) } } else { beforeShapes[shape.id] = { @@ -142,10 +147,10 @@ export function groupShapes( // TODO: This code is copied from delete.command. Create a shared helper! - const page = TLDR.getPage(data, pageId) + const { bindings } = app // We also need to delete bindings that reference the deleted shapes - Object.values(page.bindings).forEach((binding) => { + bindings.forEach((binding) => { for (const id of [binding.toId, binding.fromId]) { // If the binding references a deleted shape... if (afterShapes[id] === undefined) { @@ -154,7 +159,7 @@ export function groupShapes( afterBindings[binding.id] = undefined // Let's also look each the bound shape... - const shape = TLDR.getShape(data, id, pageId) + const shape = app.getShape(id) // If the bound shape has a handle that references the deleted binding... if (shape.handles) { diff --git a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.spec.ts b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.spec.ts index ac69d6051..178eb5787 100644 --- a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.spec.ts +++ b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.spec.ts @@ -1,19 +1,18 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { ArrowShape, SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, SessionType, TDShapeType } from '~types' describe('Move to page command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument).createPage('page2').changePage('page1') + app.loadDocument(mockDocument).createPage('page2').changePage('page1') }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.moveToPage('page2') - const currentState = state.state + const initialState = app.state + app.moveToPage('page2') + const currentState = app.state expect(currentState).toEqual(initialState) }) @@ -29,189 +28,192 @@ describe('Move to page command', () => { */ it('does, undoes and redoes command', () => { - state.select('rect1', 'rect2').moveToPage('page2') + app.select('rect1', 'rect2').moveToPage('page2') - expect(state.currentPageId).toBe('page2') - expect(state.getShape('rect1', 'page1')).toBeUndefined() - expect(state.getShape('rect1', 'page2')).toBeDefined() - expect(state.getShape('rect2', 'page1')).toBeUndefined() - expect(state.getShape('rect2', 'page2')).toBeDefined() - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) + expect(app.currentPageId).toBe('page2') + expect(app.getShape('rect1', 'page1')).toBeUndefined() + expect(app.getShape('rect1', 'page2')).toBeDefined() + expect(app.getShape('rect2', 'page1')).toBeUndefined() + expect(app.getShape('rect2', 'page2')).toBeDefined() + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) - state.undo() + app.undo() - expect(state.getShape('rect1', 'page1')).toBeDefined() - expect(state.getShape('rect1', 'page2')).toBeUndefined() - expect(state.getShape('rect2', 'page1')).toBeDefined() - expect(state.getShape('rect2', 'page2')).toBeUndefined() - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) - expect(state.currentPageId).toBe('page1') + expect(app.getShape('rect1', 'page1')).toBeDefined() + expect(app.getShape('rect1', 'page2')).toBeUndefined() + expect(app.getShape('rect2', 'page1')).toBeDefined() + expect(app.getShape('rect2', 'page2')).toBeUndefined() + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) + expect(app.currentPageId).toBe('page1') - state.redo() + app.redo() - expect(state.getShape('rect1', 'page1')).toBeUndefined() - expect(state.getShape('rect1', 'page2')).toBeDefined() - expect(state.getShape('rect2', 'page1')).toBeUndefined() - expect(state.getShape('rect2', 'page2')).toBeDefined() - expect(state.selectedIds).toStrictEqual(['rect1', 'rect2']) - expect(state.currentPageId).toBe('page2') + expect(app.getShape('rect1', 'page1')).toBeUndefined() + expect(app.getShape('rect1', 'page2')).toBeDefined() + expect(app.getShape('rect2', 'page1')).toBeUndefined() + expect(app.getShape('rect2', 'page2')).toBeDefined() + expect(app.selectedIds).toStrictEqual(['rect1', 'rect2']) + expect(app.currentPageId).toBe('page2') }) describe('when moving shapes with bindings', () => { it('deletes bindings when only the bound-to shape is moved', () => { - state + app .selectAll() .delete() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } + { type: TDShapeType.Rectangle, id: 'target1', size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const bindingId = state.bindings[0].id - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + const bindingId = app.bindings[0].id + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.select('target1').moveToPage('page2') + app.select('target1').moveToPage('page2') - expect(state.getShape('arrow1', 'page1').handles.start.bindingId).toBeUndefined() - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page1').handles.start.bindingId).toBeUndefined() + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() - state.undo() + app.undo() - expect(state.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) - expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) + expect(app.document.pages['page1'].bindings[bindingId]).toBeDefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('arrow1', 'page1').handles.start.bindingId).toBeUndefined() - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page1').handles.start.bindingId).toBeUndefined() + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() }) it('deletes bindings when only the bound-from shape is moved', () => { - state + app .selectAll() .delete() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } + { type: TDShapeType.Rectangle, id: 'target1', size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const bindingId = state.bindings[0].id - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + const bindingId = app.bindings[0].id + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.select('arrow1').moveToPage('page2') + app.select('arrow1').moveToPage('page2') - expect(state.getShape('arrow1', 'page2').handles.start.bindingId).toBeUndefined() - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page2').handles.start.bindingId).toBeUndefined() + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() - state.undo() + app.undo() - expect(state.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) - expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) + expect(app.document.pages['page1'].bindings[bindingId]).toBeDefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('arrow1', 'page2').handles.start.bindingId).toBeUndefined() - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page2').handles.start.bindingId).toBeUndefined() + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() }) it('moves bindings when both shapes are moved', () => { - state + app .selectAll() .delete() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } + { type: TDShapeType.Rectangle, id: 'target1', size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const bindingId = state.bindings[0].id - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + const bindingId = app.bindings[0].id + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.select('arrow1', 'target1').moveToPage('page2') + app.select('arrow1', 'target1').moveToPage('page2') - expect(state.getShape('arrow1', 'page2').handles.start.bindingId).toBe(bindingId) - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeDefined() + expect(app.getShape('arrow1', 'page2').handles.start.bindingId).toBe(bindingId) + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeDefined() - state.undo() + app.undo() - expect(state.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) - expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined() + expect(app.getShape('arrow1', 'page1').handles.start.bindingId).toBe(bindingId) + expect(app.document.pages['page1'].bindings[bindingId]).toBeDefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('arrow1', 'page2').handles.start.bindingId).toBe(bindingId) - expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined() - expect(state.document.pages['page2'].bindings[bindingId]).toBeDefined() + expect(app.getShape('arrow1', 'page2').handles.start.bindingId).toBe(bindingId) + expect(app.document.pages['page1'].bindings[bindingId]).toBeUndefined() + expect(app.document.pages['page2'].bindings[bindingId]).toBeDefined() }) }) describe('when moving grouped shapes', () => { it('moves groups and their children', () => { - state.group(['rect1', 'rect2'], 'groupA').moveToPage('page2') + app.group(['rect1', 'rect2'], 'groupA').moveToPage('page2') - expect(state.getShape('rect1', 'page1')).toBeUndefined() - expect(state.getShape('rect2', 'page1')).toBeUndefined() - expect(state.getShape('groupA', 'page1')).toBeUndefined() + expect(app.getShape('rect1', 'page1')).toBeUndefined() + expect(app.getShape('rect2', 'page1')).toBeUndefined() + expect(app.getShape('groupA', 'page1')).toBeUndefined() - expect(state.getShape('rect1', 'page2')).toBeDefined() - expect(state.getShape('rect2', 'page2')).toBeDefined() - expect(state.getShape('groupA', 'page2')).toBeDefined() + expect(app.getShape('rect1', 'page2')).toBeDefined() + expect(app.getShape('rect2', 'page2')).toBeDefined() + expect(app.getShape('groupA', 'page2')).toBeDefined() - state.undo() + app.undo() - expect(state.getShape('rect1', 'page2')).toBeUndefined() - expect(state.getShape('rect2', 'page2')).toBeUndefined() - expect(state.getShape('groupA', 'page2')).toBeUndefined() + expect(app.getShape('rect1', 'page2')).toBeUndefined() + expect(app.getShape('rect2', 'page2')).toBeUndefined() + expect(app.getShape('groupA', 'page2')).toBeUndefined() - expect(state.getShape('rect1', 'page1')).toBeDefined() - expect(state.getShape('rect2', 'page1')).toBeDefined() - expect(state.getShape('groupA', 'page1')).toBeDefined() + expect(app.getShape('rect1', 'page1')).toBeDefined() + expect(app.getShape('rect2', 'page1')).toBeDefined() + expect(app.getShape('groupA', 'page1')).toBeDefined() - state.redo() + app.redo() - expect(state.getShape('rect1', 'page1')).toBeUndefined() - expect(state.getShape('rect2', 'page1')).toBeUndefined() - expect(state.getShape('groupA', 'page1')).toBeUndefined() + expect(app.getShape('rect1', 'page1')).toBeUndefined() + expect(app.getShape('rect2', 'page1')).toBeUndefined() + expect(app.getShape('groupA', 'page1')).toBeUndefined() - expect(state.getShape('rect1', 'page2')).toBeDefined() - expect(state.getShape('rect2', 'page2')).toBeDefined() - expect(state.getShape('groupA', 'page2')).toBeDefined() + expect(app.getShape('rect1', 'page2')).toBeDefined() + expect(app.getShape('rect2', 'page2')).toBeDefined() + expect(app.getShape('groupA', 'page2')).toBeDefined() }) it.todo('deletes groups shapes if the groups children were all moved') it('reparents grouped shapes if the group is not moved', () => { - state.group(['rect1', 'rect2', 'rect3'], 'groupA').select('rect1').moveToPage('page2') + app.group(['rect1', 'rect2', 'rect3'], 'groupA').select('rect1').moveToPage('page2') - expect(state.getShape('rect1', 'page1')).toBeUndefined() - expect(state.getShape('rect1', 'page2')).toBeDefined() - expect(state.getShape('rect1', 'page2').parentId).toBe('page2') - expect(state.getShape('groupA', 'page1').children).toStrictEqual(['rect2', 'rect3']) + expect(app.getShape('rect1', 'page1')).toBeUndefined() + expect(app.getShape('rect1', 'page2')).toBeDefined() + expect(app.getShape('rect1', 'page2').parentId).toBe('page2') + expect(app.getShape('groupA', 'page1').children).toStrictEqual(['rect2', 'rect3']) - state.undo() + app.undo() - expect(state.getShape('rect1', 'page2')).toBeUndefined() - expect(state.getShape('rect1', 'page1').parentId).toBe('groupA') - expect(state.getShape('groupA', 'page1').children).toStrictEqual(['rect1', 'rect2', 'rect3']) + expect(app.getShape('rect1', 'page2')).toBeUndefined() + expect(app.getShape('rect1', 'page1').parentId).toBe('groupA') + expect(app.getShape('groupA', 'page1').children).toStrictEqual(['rect1', 'rect2', 'rect3']) }) }) diff --git a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts index fd0e8a71d..2cca79f30 100644 --- a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts +++ b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts @@ -1,19 +1,18 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type { ArrowShape, TLDrawSnapshot, PagePartial, TLDrawCommand, TLDrawShape } from '~types' +import type { ArrowShape, PagePartial, TldrawCommand, TDShape } from '~types' +import type { TldrawApp } from '../../internal' import { TLDR } from '~state/TLDR' import { Utils, TLBounds } from '@tldraw/core' import { Vec } from '@tldraw/vec' export function moveShapesToPage( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], viewportBounds: TLBounds, fromPageId: string, toPageId: string -): TLDrawCommand { - const { currentPageId } = data.appState - - const page = TLDR.getPage(data, currentPageId) +): TldrawCommand { + const { page } = app const fromPage: Record = { before: { @@ -39,23 +38,24 @@ export function moveShapesToPage( // Collect all the shapes to move and their keys. const movingShapeIds = new Set() - const shapesToMove = new Set() + const shapesToMove = new Set() ids - .map((id) => TLDR.getShape(data, id, fromPageId)) + .map((id) => app.getShape(id, fromPageId)) + .filter((shape) => !shape.isLocked) .forEach((shape) => { movingShapeIds.add(shape.id) shapesToMove.add(shape) if (shape.children !== undefined) { shape.children.forEach((childId) => { movingShapeIds.add(childId) - shapesToMove.add(TLDR.getShape(data, childId, fromPageId)) + shapesToMove.add(app.getShape(childId, fromPageId)) }) } }) // Where should we put start putting shapes on the "to" page? - const startingChildIndex = TLDR.getTopChildIndex(data, toPageId) + const startingChildIndex = TLDR.getTopChildIndex(app.state, toPageId) // Which shapes are we moving? const movingShapes = Array.from(shapesToMove.values()) @@ -81,7 +81,7 @@ export function moveShapesToPage( // If the shape was in a group, then pull the shape from the // parent's children array. if (shape.parentId !== fromPageId) { - const parent = TLDR.getShape(data, shape.parentId, fromPageId) + const parent = app.getShape(shape.parentId, fromPageId) fromPage.before.shapes[parent.id] = { children: parent.children, } @@ -104,7 +104,7 @@ export function moveShapesToPage( // Delete the reference from the binding's fromShape - const fromBoundShape = TLDR.getShape(data, binding.fromId, fromPageId) + const fromBoundShape = app.getShape(binding.fromId, fromPageId) // Will we be copying this binding to the new page? @@ -118,7 +118,7 @@ export function moveShapesToPage( if (movingShapeIds.has(binding.fromId)) { // If we are only moving the "from" shape, we need to delete // the binding reference from the "from" shapes handles - const fromShape = TLDR.getShape(data, binding.fromId, fromPageId) + const fromShape = app.getShape(binding.fromId, fromPageId) const handle = Object.values(fromBoundShape.handles!).find( (handle) => handle.bindingId === binding.id )! @@ -139,7 +139,7 @@ export function moveShapesToPage( } else { // If we are only moving the "to" shape, we need to delete // the binding reference from the "from" shape's handles - const fromShape = TLDR.getShape(data, binding.fromId, fromPageId) + const fromShape = app.getShape(binding.fromId, fromPageId) const handle = Object.values(fromBoundShape.handles!).find( (handle) => handle.bindingId === binding.id )! @@ -157,7 +157,7 @@ export function moveShapesToPage( // Finally, center camera on selection - const toPageState = data.document.pageStates[toPageId] + const toPageState = app.state.document.pageStates[toPageId] const bounds = Utils.getCommonBounds(movingShapes.map((shape) => TLDR.getBounds(shape))) diff --git a/packages/tldraw/src/state/commands/renamePage/renamePage.spec.ts b/packages/tldraw/src/state/commands/renamePage/renamePage.spec.ts index fa4391878..9869ebedf 100644 --- a/packages/tldraw/src/state/commands/renamePage/renamePage.spec.ts +++ b/packages/tldraw/src/state/commands/renamePage/renamePage.spec.ts @@ -1,25 +1,24 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Rename page command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() it('does, undoes and redoes command', () => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) - const initialId = state.page.id - const initialName = state.page.name + const initialId = app.page.id + const initialName = app.page.name - state.renamePage(initialId, 'My Special Page') + app.renamePage(initialId, 'My Special Page') - expect(state.page.name).toBe('My Special Page') + expect(app.page.name).toBe('My Special Page') - state.undo() + app.undo() - expect(state.page.name).toBe(initialName) + expect(app.page.name).toBe(initialName) - state.redo() + app.redo() - expect(state.page.name).toBe('My Special Page') + expect(app.page.name).toBe('My Special Page') }) }) diff --git a/packages/tldraw/src/state/commands/renamePage/renamePage.ts b/packages/tldraw/src/state/commands/renamePage/renamePage.ts index 679c57132..3461f3c81 100644 --- a/packages/tldraw/src/state/commands/renamePage/renamePage.ts +++ b/packages/tldraw/src/state/commands/renamePage/renamePage.ts @@ -1,7 +1,9 @@ -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' +import type { TldrawApp } from '../../internal' + +export function renamePage(app: TldrawApp, pageId: string, name: string): TldrawCommand { + const { page } = app -export function renamePage(data: TLDrawSnapshot, pageId: string, name: string): TLDrawCommand { - const page = data.document.pages[pageId] return { id: 'rename_page', before: { diff --git a/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.spec.ts b/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.spec.ts index ae5ced124..3fb0225ab 100644 --- a/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.spec.ts +++ b/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.spec.ts @@ -1,40 +1,40 @@ -import { TLDrawState } from '~state' -import { TLDrawSnapshot, TLDrawShapeType } from '~types' +import { TDSnapshot, TDShapeType } from '~types' import { TLDR } from '~state/TLDR' +import { TldrawTestApp } from '~test' -const state = new TLDrawState().createShapes( +const app = new TldrawTestApp().createShapes( { - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, id: 'a', childIndex: 1.0, }, { - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, id: 'b', childIndex: 2.0, }, { - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, id: 'c', childIndex: 3, }, { - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, id: 'd', childIndex: 4, } ) -const doc = { ...state.document } +const doc = { ...app.document } -function getSortedShapeIds(data: TLDrawSnapshot) { +function getSortedShapeIds(data: TDSnapshot) { return TLDR.getShapes(data, data.appState.currentPageId) .sort((a, b) => a.childIndex - b.childIndex) .map((shape) => shape.id) .join('') } -function getSortedIndices(data: TLDrawSnapshot) { +function getSortedIndices(data: TDSnapshot) { return TLDR.getShapes(data, data.appState.currentPageId) .sort((a, b) => a.childIndex - b.childIndex) .map((shape) => shape.childIndex.toFixed(2)) @@ -43,156 +43,156 @@ function getSortedIndices(data: TLDrawSnapshot) { describe('Move command', () => { beforeEach(() => { - state.loadDocument(doc) + app.loadDocument(doc) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.moveToBack() + const initialState = app.state + app.moveToBack() - const currentState = state.state + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.select('b') - state.moveToBack() - expect(getSortedShapeIds(state.state)).toBe('bacd') - state.undo() - expect(getSortedShapeIds(state.state)).toBe('abcd') - state.redo() - expect(getSortedShapeIds(state.state)).toBe('bacd') + app.select('b') + app.moveToBack() + expect(getSortedShapeIds(app.state)).toBe('bacd') + app.undo() + expect(getSortedShapeIds(app.state)).toBe('abcd') + app.redo() + expect(getSortedShapeIds(app.state)).toBe('bacd') }) describe('to back', () => { it('moves a shape to back', () => { - state.select('b') - state.moveToBack() - expect(getSortedShapeIds(state.state)).toBe('bacd') - expect(getSortedIndices(state.state)).toBe('0.50,1.00,3.00,4.00') + app.select('b') + app.moveToBack() + expect(getSortedShapeIds(app.state)).toBe('bacd') + expect(getSortedIndices(app.state)).toBe('0.50,1.00,3.00,4.00') }) it('moves two adjacent siblings to back', () => { - state.select('b', 'c') - state.moveToBack() - expect(getSortedShapeIds(state.state)).toBe('bcad') - expect(getSortedIndices(state.state)).toBe('0.33,0.67,1.00,4.00') + app.select('b', 'c') + app.moveToBack() + expect(getSortedShapeIds(app.state)).toBe('bcad') + expect(getSortedIndices(app.state)).toBe('0.33,0.67,1.00,4.00') }) it('moves two non-adjacent siblings to back', () => { - state.select('b', 'd') - state.moveToBack() - expect(getSortedShapeIds(state.state)).toBe('bdac') - expect(getSortedIndices(state.state)).toBe('0.33,0.67,1.00,3.00') + app.select('b', 'd') + app.moveToBack() + expect(getSortedShapeIds(app.state)).toBe('bdac') + expect(getSortedIndices(app.state)).toBe('0.33,0.67,1.00,3.00') }) }) describe('backward', () => { it('moves a shape backward', () => { - state.select('c') - state.moveBackward() - expect(getSortedShapeIds(state.state)).toBe('acbd') - expect(getSortedIndices(state.state)).toBe('1.00,1.50,2.00,4.00') + app.select('c') + app.moveBackward() + expect(getSortedShapeIds(app.state)).toBe('acbd') + expect(getSortedIndices(app.state)).toBe('1.00,1.50,2.00,4.00') }) it('moves a shape at first index backward', () => { - state.select('a') - state.moveBackward() - expect(getSortedShapeIds(state.state)).toBe('abcd') - expect(getSortedIndices(state.state)).toBe('1.00,2.00,3.00,4.00') + app.select('a') + app.moveBackward() + expect(getSortedShapeIds(app.state)).toBe('abcd') + expect(getSortedIndices(app.state)).toBe('1.00,2.00,3.00,4.00') }) it('moves two adjacent siblings backward', () => { - state.select('c', 'd') - state.moveBackward() - expect(getSortedShapeIds(state.state)).toBe('acdb') - expect(getSortedIndices(state.state)).toBe('1.00,1.50,1.67,2.00') + app.select('c', 'd') + app.moveBackward() + expect(getSortedShapeIds(app.state)).toBe('acdb') + expect(getSortedIndices(app.state)).toBe('1.00,1.50,1.67,2.00') }) it('moves two non-adjacent siblings backward', () => { - state.select('b', 'd') - state.moveBackward() - expect(getSortedShapeIds(state.state)).toBe('badc') - expect(getSortedIndices(state.state)).toBe('0.50,1.00,2.50,3.00') + app.select('b', 'd') + app.moveBackward() + expect(getSortedShapeIds(app.state)).toBe('badc') + expect(getSortedIndices(app.state)).toBe('0.50,1.00,2.50,3.00') }) it('moves two adjacent siblings backward at zero index', () => { - state.select('a', 'b') - state.moveBackward() - expect(getSortedShapeIds(state.state)).toBe('abcd') - expect(getSortedIndices(state.state)).toBe('1.00,2.00,3.00,4.00') + app.select('a', 'b') + app.moveBackward() + expect(getSortedShapeIds(app.state)).toBe('abcd') + expect(getSortedIndices(app.state)).toBe('1.00,2.00,3.00,4.00') }) }) describe('forward', () => { it('moves a shape forward', () => { - state.select('c') - state.moveForward() - expect(getSortedShapeIds(state.state)).toBe('abdc') - expect(getSortedIndices(state.state)).toBe('1.00,2.00,4.00,5.00') + app.select('c') + app.moveForward() + expect(getSortedShapeIds(app.state)).toBe('abdc') + expect(getSortedIndices(app.state)).toBe('1.00,2.00,4.00,5.00') }) it('moves a shape forward at the top index', () => { - state.select('b') - state.moveForward() - state.moveForward() - state.moveForward() - expect(getSortedShapeIds(state.state)).toBe('acdb') - expect(getSortedIndices(state.state)).toBe('1.00,3.00,4.00,5.00') + app.select('b') + app.moveForward() + app.moveForward() + app.moveForward() + expect(getSortedShapeIds(app.state)).toBe('acdb') + expect(getSortedIndices(app.state)).toBe('1.00,3.00,4.00,5.00') }) it('moves two adjacent siblings forward', () => { - state.select('a', 'b') - state.moveForward() - expect(getSortedShapeIds(state.state)).toBe('cabd') - expect(getSortedIndices(state.state)).toBe('3.00,3.33,3.50,4.00') + app.select('a', 'b') + app.moveForward() + expect(getSortedShapeIds(app.state)).toBe('cabd') + expect(getSortedIndices(app.state)).toBe('3.00,3.33,3.50,4.00') }) it('moves two non-adjacent siblings forward', () => { - state.select('a', 'c') - state.moveForward() - expect(getSortedShapeIds(state.state)).toBe('badc') - expect(getSortedIndices(state.state)).toBe('2.00,2.50,4.00,5.00') + app.select('a', 'c') + app.moveForward() + expect(getSortedShapeIds(app.state)).toBe('badc') + expect(getSortedIndices(app.state)).toBe('2.00,2.50,4.00,5.00') }) it('moves two adjacent siblings forward at top index', () => { - state.select('c', 'd') - state.moveForward() - expect(getSortedShapeIds(state.state)).toBe('abcd') - expect(getSortedIndices(state.state)).toBe('1.00,2.00,3.00,4.00') + app.select('c', 'd') + app.moveForward() + expect(getSortedShapeIds(app.state)).toBe('abcd') + expect(getSortedIndices(app.state)).toBe('1.00,2.00,3.00,4.00') }) }) describe('to front', () => { it('moves a shape to front', () => { - state.select('b') - state.moveToFront() - expect(getSortedShapeIds(state.state)).toBe('acdb') - expect(getSortedIndices(state.state)).toBe('1.00,3.00,4.00,5.00') + app.select('b') + app.moveToFront() + expect(getSortedShapeIds(app.state)).toBe('acdb') + expect(getSortedIndices(app.state)).toBe('1.00,3.00,4.00,5.00') }) it('moves two adjacent siblings to front', () => { - state.select('a', 'b') - state.moveToFront() - expect(getSortedShapeIds(state.state)).toBe('cdab') - expect(getSortedIndices(state.state)).toBe('3.00,4.00,5.00,6.00') + app.select('a', 'b') + app.moveToFront() + expect(getSortedShapeIds(app.state)).toBe('cdab') + expect(getSortedIndices(app.state)).toBe('3.00,4.00,5.00,6.00') }) it('moves two non-adjacent siblings to front', () => { - state.select('a', 'c') - state.moveToFront() - expect(getSortedShapeIds(state.state)).toBe('bdac') - expect(getSortedIndices(state.state)).toBe('2.00,4.00,5.00,6.00') + app.select('a', 'c') + app.moveToFront() + expect(getSortedShapeIds(app.state)).toBe('bdac') + expect(getSortedIndices(app.state)).toBe('2.00,4.00,5.00,6.00') }) it('moves siblings already at front to front', () => { - state.select('c', 'd') - state.moveToFront() - expect(getSortedShapeIds(state.state)).toBe('abcd') - expect(getSortedIndices(state.state)).toBe('1.00,2.00,3.00,4.00') + app.select('c', 'd') + app.moveToFront() + expect(getSortedShapeIds(app.state)).toBe('abcd') + expect(getSortedIndices(app.state)).toBe('1.00,2.00,3.00,4.00') }) }) }) diff --git a/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.ts b/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.ts index c3cf38fd5..2a5e61989 100644 --- a/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.ts +++ b/packages/tldraw/src/state/commands/reorderShapes/reorderShapes.ts @@ -1,34 +1,33 @@ -import { MoveType, TLDrawSnapshot, TLDrawShape, TLDrawCommand } from '~types' +import { MoveType, TDShape, TldrawCommand } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '../../internal' -export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveType): TLDrawCommand { - const { currentPageId } = data.appState +export function reorderShapes(app: TldrawApp, ids: string[], type: MoveType): TldrawCommand { + const { currentPageId, page } = app // Get the unique parent ids for the selected elements - const parentIds = new Set(ids.map((id) => TLDR.getShape(data, id, currentPageId).parentId)) + const parentIds = new Set(ids.map((id) => app.getShape(id).parentId)) let result: { - before: Record> - after: Record> + before: Record> + after: Record> } = { before: {}, after: {} } let startIndex: number let startChildIndex: number let step: number - const page = TLDR.getPage(data, currentPageId) - // Collect shapes with common parents into a table under their parent id Array.from(parentIds.values()).forEach((parentId) => { - let sortedChildren: TLDrawShape[] = [] + let sortedChildren: TDShape[] = [] if (parentId === page.id) { sortedChildren = Object.values(page.shapes).sort((a, b) => a.childIndex - b.childIndex) } else { - const parent = TLDR.getShape(data, parentId, currentPageId) + const parent = app.getShape(parentId) if (!parent.children) throw Error('No children in parent!') sortedChildren = parent.children - .map((childId) => TLDR.getShape(data, childId, currentPageId)) + .map((childId) => app.getShape(childId)) .sort((a, b) => a.childIndex - b.childIndex) } @@ -63,7 +62,7 @@ export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveTyp // Get the results of moving the selected shapes below the first open index's shape result = TLDR.mutateShapes( - data, + app.state, sortedIndicesToMove.map((i) => sortedChildren[i].id).reverse(), (_shape, i) => ({ childIndex: startChildIndex - (i + 1) * step, @@ -94,7 +93,7 @@ export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveTyp // Get the results of moving the selected shapes below the first open index's shape result = TLDR.mutateShapes( - data, + app.state, sortedIndicesToMove.map((i) => sortedChildren[i].id), (_shape, i) => ({ childIndex: startChildIndex + (i + 1), @@ -153,7 +152,7 @@ export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveTyp if (Object.values(indexMap).length > 0) { // Get the results of moving the selected shapes below the first open index's shape result = TLDR.mutateShapes( - data, + app.state, sortedIndicesToMove.map((i) => sortedChildren[i].id), (shape) => ({ childIndex: indexMap[shape.id], @@ -202,7 +201,7 @@ export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveTyp if (Object.values(indexMap).length > 0) { // Get the results of moving the selected shapes below the first open index's shape result = TLDR.mutateShapes( - data, + app.state, sortedIndicesToMove.map((i) => sortedChildren[i].id), (shape) => ({ childIndex: indexMap[shape.id], diff --git a/packages/tldraw/src/state/commands/resetBounds/resetBounds.spec.ts b/packages/tldraw/src/state/commands/resetBounds/resetBounds.spec.ts index 2f91f6927..1d80a6673 100644 --- a/packages/tldraw/src/state/commands/resetBounds/resetBounds.spec.ts +++ b/packages/tldraw/src/state/commands/resetBounds/resetBounds.spec.ts @@ -1,37 +1,37 @@ import { TLBoundsCorner, Utils } from '@tldraw/core' -import { TLDrawState } from '~state' import { TLDR } from '~state/TLDR' -import { mockDocument } from '~test' -import { SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDShapeType } from '~types' describe('Reset bounds command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) it('does, undoes and redoes command', () => { - state.createShapes({ + app.createShapes({ id: 'text1', - type: TLDrawShapeType.Text, + type: TDShapeType.Text, point: [0, 0], text: 'Hello World', }) // Scale is undefined by default - expect(state.getShape('text1').style.scale).toBeUndefined() + expect(app.getShape('text1').style.scale).toBe(1) // Transform the shape in order to change its point and scale - state + app .select('text1') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([-100, -100], false, false) + .movePointer([0, 0]) + .startSession(SessionType.Transform, TLBoundsCorner.TopLeft) + .movePointer({ x: -100, y: -100, shiftKey: false, altKey: false }) .completeSession() - const scale = state.getShape('text1').style.scale - const bounds = TLDR.getBounds(state.getShape('text1')) + const scale = app.getShape('text1').style.scale + const bounds = TLDR.getBounds(app.getShape('text1')) const center = Utils.getBoundsCenter(bounds) expect(scale).not.toBe(1) @@ -39,25 +39,25 @@ describe('Reset bounds command', () => { // Reset the bounds - state.resetBounds(['text1']) + app.resetBounds(['text1']) // The scale should be back to 1 - expect(state.getShape('text1').style.scale).toBe(1) + expect(app.getShape('text1').style.scale).toBe(1) // The centers should be the same - expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center) + expect(Utils.getBoundsCenter(TLDR.getBounds(app.getShape('text1')))).toStrictEqual(center) - state.undo() + app.undo() // The scale should be what it was before - expect(state.getShape('text1').style.scale).not.toBe(1) + expect(app.getShape('text1').style.scale).not.toBe(1) // The centers should be the same - expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center) + expect(Utils.getBoundsCenter(TLDR.getBounds(app.getShape('text1')))).toStrictEqual(center) - state.redo() + app.redo() // The scale should be back to 1 - expect(state.getShape('text1').style.scale).toBe(1) + expect(app.getShape('text1').style.scale).toBe(1) // The centers should be the same - expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center) + expect(Utils.getBoundsCenter(TLDR.getBounds(app.getShape('text1')))).toStrictEqual(center) }) }) diff --git a/packages/tldraw/src/state/commands/resetBounds/resetBounds.ts b/packages/tldraw/src/state/commands/resetBounds/resetBounds.ts index 7010fa541..432d29b8c 100644 --- a/packages/tldraw/src/state/commands/resetBounds/resetBounds.ts +++ b/packages/tldraw/src/state/commands/resetBounds/resetBounds.ts @@ -1,13 +1,14 @@ -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TldrawCommand } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '../../internal' -export function resetBounds(data: TLDrawSnapshot, ids: string[], pageId: string): TLDrawCommand { - const { currentPageId } = data.appState +export function resetBounds(app: TldrawApp, ids: string[], pageId: string): TldrawCommand { + const { currentPageId } = app const { before, after } = TLDR.mutateShapes( - data, + app.state, ids, - (shape) => TLDR.getShapeUtils(shape).onDoubleClickBoundsHandle?.(shape), + (shape) => app.getShapeUtil(shape).onDoubleClickBoundsHandle?.(shape), pageId ) @@ -16,7 +17,7 @@ export function resetBounds(data: TLDrawSnapshot, ids: string[], pageId: string) before: { document: { pages: { - [data.appState.currentPageId]: { shapes: before }, + [currentPageId]: { shapes: before }, }, pageStates: { [currentPageId]: { @@ -28,7 +29,7 @@ export function resetBounds(data: TLDrawSnapshot, ids: string[], pageId: string) after: { document: { pages: { - [data.appState.currentPageId]: { shapes: after }, + [currentPageId]: { shapes: after }, }, pageStates: { [currentPageId]: { diff --git a/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.spec.ts b/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.spec.ts index 1471ce699..567a2543b 100644 --- a/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.spec.ts +++ b/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.spec.ts @@ -1,39 +1,38 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Rotate command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.rotate() - const currentState = state.state + const initialState = app.state + app.rotate() + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.select('rect1') + app.select('rect1') - expect(state.getShape('rect1').rotation).toBe(undefined) + expect(app.getShape('rect1').rotation).toBe(undefined) - state.rotate() + app.rotate() - expect(state.getShape('rect1').rotation).toBe(Math.PI * (6 / 4)) + expect(app.getShape('rect1').rotation).toBe(Math.PI * (6 / 4)) - state.undo() + app.undo() - expect(state.getShape('rect1').rotation).toBe(undefined) + expect(app.getShape('rect1').rotation).toBe(undefined) - state.redo() + app.redo() - expect(state.getShape('rect1').rotation).toBe(Math.PI * (6 / 4)) + expect(app.getShape('rect1').rotation).toBe(Math.PI * (6 / 4)) }) it.todo('Rotates several shapes at once.') @@ -43,17 +42,17 @@ describe('Rotate command', () => { describe('when running the command', () => { it('restores selection on undo', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') .rotate() .selectNone() .undo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) - state.selectNone().redo() + app.selectNone().redo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) }) }) diff --git a/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.ts b/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.ts index a9971f497..1a0317b17 100644 --- a/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.ts +++ b/packages/tldraw/src/state/commands/rotateShapes/rotateShapes.ts @@ -1,30 +1,31 @@ import { Utils } from '@tldraw/core' -import type { TLDrawCommand, TLDrawSnapshot, TLDrawShape } from '~types' +import type { TldrawCommand, TDShape } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '../../internal' const PI2 = Math.PI * 2 export function rotateShapes( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], delta = -PI2 / 4 -): TLDrawCommand | void { - const { currentPageId } = data.appState +): TldrawCommand | void { + const { currentPageId } = app // The shapes for the before patch - const before: Record> = {} + const before: Record> = {} // The shapes for the after patch - const after: Record> = {} + const after: Record> = {} // Find the shapes that we want to rotate. // We don't rotate groups: we rotate their children instead. - const shapesToRotate = ids.flatMap((id) => { - const shape = TLDR.getShape(data, id, currentPageId) - return shape.children - ? shape.children.map((childId) => TLDR.getShape(data, childId, currentPageId)) - : shape - }) + const shapesToRotate = ids + .flatMap((id) => { + const shape = app.getShape(id) + return shape.children ? shape.children.map((childId) => app.getShape(childId)) : shape + }) + .filter((shape) => !shape.isLocked) // Find the common center to all shapes // This is the point that we'll rotate around diff --git a/packages/tldraw/src/state/commands/shared/removeShapesFromPage.ts b/packages/tldraw/src/state/commands/shared/removeShapesFromPage.ts index 7a53bfed3..5be147316 100644 --- a/packages/tldraw/src/state/commands/shared/removeShapesFromPage.ts +++ b/packages/tldraw/src/state/commands/shared/removeShapesFromPage.ts @@ -1,7 +1,7 @@ import { TLDR } from '~state/TLDR' -import type { ArrowShape, TLDrawSnapshot, GroupShape, PagePartial } from '~types' +import type { ArrowShape, TDSnapshot, GroupShape, PagePartial } from '~types' -export function removeShapesFromPage(data: TLDrawSnapshot, ids: string[], pageId: string) { +export function removeShapesFromPage(data: TDSnapshot, ids: string[], pageId: string) { const before: PagePartial = { shapes: {}, bindings: {}, @@ -18,33 +18,40 @@ export function removeShapesFromPage(data: TLDrawSnapshot, ids: string[], pageId // These are the shapes we're definitely going to delete - ids.forEach((id) => { - deletedIds.add(id) - const shape = TLDR.getShape(data, id, pageId) - before.shapes[id] = shape - after.shapes[id] = undefined + ids + .filter((id) => !TLDR.getShape(data, id, pageId).isLocked) + .forEach((id) => { + deletedIds.add(id) + const shape = TLDR.getShape(data, id, pageId) + before.shapes[id] = shape + after.shapes[id] = undefined - // Also delete the shape's children + // Also delete the shape's children - if (shape.children !== undefined) { - shape.children.forEach((childId) => { - deletedIds.add(childId) - const child = TLDR.getShape(data, childId, pageId) - before.shapes[childId] = child - after.shapes[childId] = undefined - }) - } + if (shape.children !== undefined) { + shape.children.forEach((childId) => { + deletedIds.add(childId) + const child = TLDR.getShape(data, childId, pageId) + before.shapes[childId] = child + after.shapes[childId] = undefined + }) + } - if (shape.parentId !== pageId) { - parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId)) - } - }) + if (shape.parentId !== pageId) { + parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId)) + } + }) parentsToUpdate.forEach((parent) => { if (ids.includes(parent.id)) return deletedIds.add(parent.id) before.shapes[parent.id] = { children: parent.children } after.shapes[parent.id] = { children: parent.children.filter((id) => !ids.includes(id)) } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (after.shapes[parent.id]?.children!.length === 0) { + after.shapes[parent.id] = undefined + before.shapes[parent.id] = TLDR.getShape(data, parent.id, pageId) + } }) // Recursively check for empty parents? diff --git a/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.spec.ts b/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.spec.ts index 891d9ef66..aa886e285 100644 --- a/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.spec.ts +++ b/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.spec.ts @@ -1,104 +1,101 @@ -import { StretchType, RectangleShape, TLDrawShapeType } from '~types' -import { TLDrawState } from '~state' -import { mockDocument, TLDrawStateUtils } from '~test' -import Vec from '@tldraw/vec' +import { StretchType, RectangleShape, TDShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' describe('Stretch command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when less than two shapes are selected', () => { it('does nothing', () => { - state.select('rect2') - const initialState = state.state - state.stretch(StretchType.Horizontal) - const currentState = state.state + app.select('rect2') + const initialState = app.state + app.stretch(StretchType.Horizontal) + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.select('rect1', 'rect2') - state.stretch(StretchType.Horizontal) + app.select('rect1', 'rect2') + app.stretch(StretchType.Horizontal) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').size).toStrictEqual([200, 100]) - expect(state.getShape('rect2').point).toStrictEqual([0, 100]) - expect(state.getShape('rect2').size).toStrictEqual([200, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').size).toStrictEqual([200, 100]) + expect(app.getShape('rect2').point).toStrictEqual([0, 100]) + expect(app.getShape('rect2').size).toStrictEqual([200, 100]) - state.undo() + app.undo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').size).toStrictEqual([100, 100]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) - expect(state.getShape('rect2').size).toStrictEqual([100, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').size).toStrictEqual([100, 100]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('rect2').size).toStrictEqual([100, 100]) - state.redo() + app.redo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').size).toStrictEqual([200, 100]) - expect(state.getShape('rect2').point).toStrictEqual([0, 100]) - expect(state.getShape('rect2').size).toStrictEqual([200, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').size).toStrictEqual([200, 100]) + expect(app.getShape('rect2').point).toStrictEqual([0, 100]) + expect(app.getShape('rect2').size).toStrictEqual([200, 100]) }) it('stretches horizontally', () => { - state.select('rect1', 'rect2') - state.stretch(StretchType.Horizontal) + app.select('rect1', 'rect2') + app.stretch(StretchType.Horizontal) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').size).toStrictEqual([200, 100]) - expect(state.getShape('rect2').point).toStrictEqual([0, 100]) - expect(state.getShape('rect2').size).toStrictEqual([200, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').size).toStrictEqual([200, 100]) + expect(app.getShape('rect2').point).toStrictEqual([0, 100]) + expect(app.getShape('rect2').size).toStrictEqual([200, 100]) }) it('stretches vertically', () => { - state.select('rect1', 'rect2') - state.stretch(StretchType.Vertical) + app.select('rect1', 'rect2') + app.stretch(StretchType.Vertical) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').size).toStrictEqual([100, 200]) - expect(state.getShape('rect2').point).toStrictEqual([100, 0]) - expect(state.getShape('rect2').size).toStrictEqual([100, 200]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').size).toStrictEqual([100, 200]) + expect(app.getShape('rect2').point).toStrictEqual([100, 0]) + expect(app.getShape('rect2').size).toStrictEqual([100, 200]) }) }) describe('when running the command', () => { it('restores selection on undo', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') .stretch(StretchType.Horizontal) .selectNone() .undo() - expect(state.selectedIds).toEqual(['rect1', 'rect2']) + expect(app.selectedIds).toEqual(['rect1', 'rect2']) - state.selectNone().redo() + app.selectNone().redo() - expect(state.selectedIds).toEqual(['rect1', 'rect2']) + expect(app.selectedIds).toEqual(['rect1', 'rect2']) }) }) describe('when stretching groups', () => { it('stretches children', () => { - const state = new TLDrawState() + new TldrawTestApp() .createShapes( - { id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] }, - { id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] }, - { id: 'rect3', type: TLDrawShapeType.Rectangle, point: [200, 200], size: [100, 100] } + { id: 'rect1', type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100] }, + { id: 'rect2', type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100] }, + { id: 'rect3', type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100] } ) .group(['rect1', 'rect2'], 'groupA') .selectAll() .stretch(StretchType.Vertical) - - new TLDrawStateUtils(state).expectShapesToHaveProps({ - rect1: { point: [0, 0], size: [100, 300] }, - rect2: { point: [100, 0], size: [100, 300] }, - rect3: { point: [200, 0], size: [100, 300] }, - }) + .expectShapesToHaveProps({ + rect1: { point: [0, 0], size: [100, 300] }, + rect2: { point: [100, 0], size: [100, 300] }, + rect3: { point: [200, 0], size: [100, 300] }, + }) }) }) diff --git a/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.ts b/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.ts index 1221d9ea1..c6f8291d3 100644 --- a/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.ts +++ b/packages/tldraw/src/state/commands/stretchShapes/stretchShapes.ts @@ -1,29 +1,28 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TLBoundsCorner, Utils } from '@tldraw/core' -import { StretchType, TLDrawShapeType } from '~types' -import type { TLDrawSnapshot, TLDrawCommand } from '~types' +import { StretchType, TDShapeType } from '~types' +import type { TldrawCommand } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '../../internal' -export function stretchShapes( - data: TLDrawSnapshot, - ids: string[], - type: StretchType -): TLDrawCommand { - const { currentPageId } = data.appState +export function stretchShapes(app: TldrawApp, ids: string[], type: StretchType): TldrawCommand { + const { currentPageId, selectedIds } = app - const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId)) + const initialShapes = ids.map((id) => app.getShape(id)) const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) const commonBounds = Utils.getCommonBounds(boundsForShapes) - const idsToMutate = ids.flatMap((id) => { - const shape = TLDR.getShape(data, id, currentPageId) - return shape.children ? shape.children : shape.id - }) + const idsToMutate = ids + .flatMap((id) => { + const shape = app.getShape(id) + return shape.children ? shape.children : shape.id + }) + .filter((id) => !app.getShape(id).isLocked) const { before, after } = TLDR.mutateShapes( - data, + app.state, idsToMutate, (shape) => { const bounds = TLDR.getBounds(shape) @@ -37,7 +36,7 @@ export function stretchShapes( width: commonBounds.width, } - return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { + return TLDR.getShapeUtil(shape).transformSingle(shape, newBounds, { type: TLBoundsCorner.TopLeft, scaleX: newBounds.width / bounds.width, scaleY: 1, @@ -53,7 +52,7 @@ export function stretchShapes( height: commonBounds.height, } - return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { + return TLDR.getShapeUtil(shape).transformSingle(shape, newBounds, { type: TLBoundsCorner.TopLeft, scaleX: 1, scaleY: newBounds.height / bounds.height, @@ -67,7 +66,7 @@ export function stretchShapes( ) initialShapes.forEach((shape) => { - if (shape.type === TLDrawShapeType.Group) { + if (shape.type === TDShapeType.Group) { delete before[shape.id] delete after[shape.id] } @@ -82,7 +81,7 @@ export function stretchShapes( }, pageStates: { [currentPageId]: { - selectedIds: ids, + selectedIds, }, }, }, diff --git a/packages/tldraw/src/state/commands/styleShapes/styleShapes.spec.ts b/packages/tldraw/src/state/commands/styleShapes/styleShapes.spec.ts index eae919601..566a63a02 100644 --- a/packages/tldraw/src/state/commands/styleShapes/styleShapes.spec.ts +++ b/packages/tldraw/src/state/commands/styleShapes/styleShapes.spec.ts @@ -1,73 +1,70 @@ -import { TLDrawState } from '~state' import { TLDR } from '~state/TLDR' -import { mockDocument } from '~test' -import { SizeStyle, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SizeStyle, TDShapeType } from '~types' describe('Style command', () => { it('does, undoes and redoes command', () => { - const state = new TLDrawState() - state.loadDocument(mockDocument) - state.select('rect1') - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium) + const app = new TldrawTestApp().loadDocument(mockDocument).select('rect1') + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Medium) - state.style({ size: SizeStyle.Small }) + app.style({ size: SizeStyle.Small }) - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Small) - state.undo() + app.undo() - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Medium) - state.redo() + app.redo() - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Small) }) describe('When styling groups', () => { it('applies style to all group children', () => { - const state = new TLDrawState() - state + const app = new TldrawTestApp() + app .loadDocument(mockDocument) .group(['rect1', 'rect2'], 'groupA') .select('groupA') .style({ size: SizeStyle.Small }) - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small) - expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect2').style.size).toEqual(SizeStyle.Small) - state.undo() + app.undo() - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium) - expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Medium) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Medium) + expect(app.getShape('rect2').style.size).toEqual(SizeStyle.Medium) - state.redo() + app.redo() - expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small) - expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect1').style.size).toEqual(SizeStyle.Small) + expect(app.getShape('rect2').style.size).toEqual(SizeStyle.Small) }) }) describe('When styling text', () => { it('recenters the shape if the size changed', () => { - const state = new TLDrawState().createShapes({ + const app = new TldrawTestApp().createShapes({ id: 'text1', - type: TLDrawShapeType.Text, + type: TDShapeType.Text, text: 'Hello world', }) - const centerA = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1')) + const centerA = TLDR.getShapeUtil(TDShapeType.Text).getCenter(app.getShape('text1')) - state.select('text1').style({ size: SizeStyle.Large }) + app.select('text1').style({ size: SizeStyle.Large }) - const centerB = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1')) + const centerB = TLDR.getShapeUtil(TDShapeType.Text).getCenter(app.getShape('text1')) - state.style({ size: SizeStyle.Small }) + app.style({ size: SizeStyle.Small }) - const centerC = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1')) + const centerC = TLDR.getShapeUtil(TDShapeType.Text).getCenter(app.getShape('text1')) - state.style({ size: SizeStyle.Medium }) + app.style({ size: SizeStyle.Medium }) - const centerD = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1')) + const centerD = TLDR.getShapeUtil(TDShapeType.Text).getCenter(app.getShape('text1')) expect(centerA).toEqual(centerB) expect(centerA).toEqual(centerC) @@ -78,17 +75,17 @@ describe('Style command', () => { describe('when running the command', () => { it('restores selection on undo', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') .style({ size: SizeStyle.Small }) .selectNone() .undo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) - state.selectNone().redo() + app.selectNone().redo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) }) }) diff --git a/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts b/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts index f64be5dcf..706b30bf4 100644 --- a/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts +++ b/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts @@ -1,36 +1,25 @@ -import { - ShapeStyles, - TLDrawCommand, - TLDrawSnapshot, - TLDrawShape, - TLDrawShapeType, - TextShape, -} from '~types' +import { ShapeStyles, TldrawCommand, TDShape, TDShapeType, TextShape } from '~types' import { TLDR } from '~state/TLDR' -import type { Patch } from 'rko' import Vec from '@tldraw/vec' +import type { Patch } from 'rko' +import type { TldrawApp } from '../../internal' export function styleShapes( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], changes: Partial -): TLDrawCommand { - const { currentPageId } = data.appState +): TldrawCommand { + const { currentPageId, selectedIds } = app - const shapeIdsToMutate = ids.flatMap((id) => TLDR.getDocumentBranch(data, id, currentPageId)) + const shapeIdsToMutate = ids + .flatMap((id) => TLDR.getDocumentBranch(app.state, id, currentPageId)) + .filter((id) => !app.getShape(id).isLocked) - // const { before, after } = TLDR.mutateShapes( - // data, - // shapeIdsToMutate, - // (shape) => ({ style: { ...shape.style, ...changes } }), - // currentPageId - // ) - - const beforeShapes: Record> = {} - const afterShapes: Record> = {} + const beforeShapes: Record> = {} + const afterShapes: Record> = {} shapeIdsToMutate - .map((id) => TLDR.getShape(data, id, currentPageId)) + .map((id) => app.getShape(id)) .filter((shape) => !shape.isLocked) .forEach((shape) => { beforeShapes[shape.id] = { @@ -45,14 +34,14 @@ export function styleShapes( style: changes, } - if (shape.type === TLDrawShapeType.Text) { + if (shape.type === TDShapeType.Text) { beforeShapes[shape.id].point = shape.point afterShapes[shape.id].point = Vec.round( Vec.add( shape.point, Vec.sub( - TLDR.getShapeUtils(shape).getCenter(shape), - TLDR.getShapeUtils(shape).getCenter({ + app.getShapeUtil(shape).getCenter(shape), + app.getShapeUtil(shape).getCenter({ ...shape, style: { ...shape.style, ...changes }, } as TextShape) @@ -73,12 +62,12 @@ export function styleShapes( }, pageStates: { [currentPageId]: { - selectedIds: ids, + selectedIds: selectedIds, }, }, }, appState: { - currentStyle: { ...data.appState.currentStyle }, + currentStyle: { ...app.appState.currentStyle }, }, }, after: { @@ -95,7 +84,7 @@ export function styleShapes( }, }, appState: { - currentStyle: { ...data.appState.currentStyle, ...changes }, + currentStyle: changes, }, }, } diff --git a/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.spec.ts b/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.spec.ts index bffb2eda7..201605df4 100644 --- a/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.spec.ts +++ b/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.spec.ts @@ -1,20 +1,13 @@ -import { TLDR } from '~state/TLDR' -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { ArrowShape, Decoration, TLDrawShape, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, Decoration, TDShapeType } from '~types' describe('Toggle decoration command', () => { - const state = new TLDrawState() - - beforeEach(() => { - state.loadDocument(mockDocument) - }) - describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.toggleDecoration('start') - const currentState = state.state + const app = new TldrawTestApp() + const initialState = app.state + app.toggleDecoration('start') + const currentState = app.state expect(currentState).toEqual(initialState) }) @@ -22,34 +15,35 @@ describe('Toggle decoration command', () => { describe('when handle id is invalid', () => { it('does nothing', () => { - const initialState = state.state - state.toggleDecoration('invalid') - const currentState = state.state + const app = new TldrawTestApp() + const initialState = app.state + app.toggleDecoration('invalid') + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state + const app = new TldrawTestApp() .createShapes({ id: 'arrow1', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, }) .select('arrow1') - expect(state.getShape('arrow1').decorations?.end).toBe(Decoration.Arrow) + expect(app.getShape('arrow1').decorations?.end).toBe(Decoration.Arrow) - state.toggleDecoration('end') + app.toggleDecoration('end') - expect(state.getShape('arrow1').decorations?.end).toBe(undefined) + expect(app.getShape('arrow1').decorations?.end).toBe(undefined) - state.undo() + app.undo() - expect(state.getShape('arrow1').decorations?.end).toBe(Decoration.Arrow) + expect(app.getShape('arrow1').decorations?.end).toBe(Decoration.Arrow) - state.redo() + app.redo() - expect(state.getShape('arrow1').decorations?.end).toBe(undefined) + expect(app.getShape('arrow1').decorations?.end).toBe(undefined) }) }) diff --git a/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.ts b/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.ts index 0cc216b77..c5579e5d9 100644 --- a/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.ts +++ b/packages/tldraw/src/state/commands/toggleShapesDecoration/toggleShapesDecoration.ts @@ -1,41 +1,39 @@ import { Decoration } from '~types' -import type { ArrowShape, TLDrawCommand, TLDrawSnapshot } from '~types' -import { TLDR } from '~state/TLDR' +import type { ArrowShape, TldrawCommand } from '~types' import type { Patch } from 'rko' +import type { TldrawApp } from '../../internal' export function toggleShapesDecoration( - data: TLDrawSnapshot, + app: TldrawApp, ids: string[], decorationId: 'start' | 'end' -): TLDrawCommand { - const { currentPageId } = data.appState +): TldrawCommand { + const { currentPageId, selectedIds } = app const beforeShapes: Record> = Object.fromEntries( ids.map((id) => [ id, { decorations: { - [decorationId]: TLDR.getShape(data, id, currentPageId).decorations?.[ - decorationId - ], + [decorationId]: app.getShape(id).decorations?.[decorationId], }, }, ]) ) const afterShapes: Record> = Object.fromEntries( - ids.map((id) => [ - id, - { - decorations: { - [decorationId]: TLDR.getShape(data, id, currentPageId).decorations?.[ - decorationId - ] - ? undefined - : Decoration.Arrow, + ids + .filter((id) => !app.getShape(id).isLocked) + .map((id) => [ + id, + { + decorations: { + [decorationId]: app.getShape(id).decorations?.[decorationId] + ? undefined + : Decoration.Arrow, + }, }, - }, - ]) + ]) ) return { @@ -47,7 +45,7 @@ export function toggleShapesDecoration( }, pageStates: { [currentPageId]: { - selectedIds: ids, + selectedIds, }, }, }, diff --git a/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.spec.ts b/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.spec.ts index 9fcd40334..6d3607e76 100644 --- a/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.spec.ts +++ b/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.spec.ts @@ -1,79 +1,78 @@ import type { RectangleShape } from '~types' -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Toggle command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.toggleHidden() - const currentState = state.state + const initialState = app.state + app.toggleHidden() + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.selectAll() + app.selectAll() - expect(state.getShape('rect2').isLocked).toBe(undefined) + expect(app.getShape('rect2').isLocked).toBe(undefined) - state.toggleLocked() + app.toggleLocked() - expect(state.getShape('rect2').isLocked).toBe(true) + expect(app.getShape('rect2').isLocked).toBe(true) - state.undo() + app.undo() - expect(state.getShape('rect2').isLocked).toBe(undefined) + expect(app.getShape('rect2').isLocked).toBe(undefined) - state.redo() + app.redo() - expect(state.getShape('rect2').isLocked).toBe(true) + expect(app.getShape('rect2').isLocked).toBe(true) }) it('toggles on before off when mixed values', () => { - state.select('rect2') + app.select('rect2') - expect(state.getShape('rect1').isAspectRatioLocked).toBe(undefined) - expect(state.getShape('rect2').isAspectRatioLocked).toBe(undefined) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(undefined) + expect(app.getShape('rect2').isAspectRatioLocked).toBe(undefined) - state.toggleAspectRatioLocked() + app.toggleAspectRatioLocked() - expect(state.getShape('rect1').isAspectRatioLocked).toBe(undefined) - expect(state.getShape('rect2').isAspectRatioLocked).toBe(true) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(undefined) + expect(app.getShape('rect2').isAspectRatioLocked).toBe(true) - state.selectAll() - state.toggleAspectRatioLocked() + app.selectAll() + app.toggleAspectRatioLocked() - expect(state.getShape('rect1').isAspectRatioLocked).toBe(true) - expect(state.getShape('rect1').isAspectRatioLocked).toBe(true) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(true) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(true) - state.toggleAspectRatioLocked() + app.toggleAspectRatioLocked() - expect(state.getShape('rect1').isAspectRatioLocked).toBe(false) - expect(state.getShape('rect1').isAspectRatioLocked).toBe(false) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(false) + expect(app.getShape('rect1').isAspectRatioLocked).toBe(false) }) }) describe('when running the command', () => { it('restores selection on undo', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') .toggleHidden() .selectNone() .undo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) - state.selectNone().redo() + app.selectNone().redo() - expect(state.selectedIds).toEqual(['rect1']) + expect(app.selectedIds).toEqual(['rect1']) }) }) diff --git a/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.ts b/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.ts index b1ae4c5ea..7f12288dd 100644 --- a/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.ts +++ b/packages/tldraw/src/state/commands/toggleShapesProp/toggleShapesProp.ts @@ -1,20 +1,17 @@ -import type { TLDrawShape, TLDrawSnapshot, TLDrawCommand } from '~types' +import type { TDShape, TldrawCommand } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '~state' -export function toggleShapeProp( - data: TLDrawSnapshot, - ids: string[], - prop: keyof TLDrawShape -): TLDrawCommand { - const { currentPageId } = data.appState +export function toggleShapeProp(app: TldrawApp, ids: string[], prop: keyof TDShape): TldrawCommand { + const { currentPageId } = app - const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId)) + const initialShapes = ids.map((id) => app.getShape(id)) const isAllToggled = initialShapes.every((shape) => shape[prop]) const { before, after } = TLDR.mutateShapes( - data, - TLDR.getSelectedIds(data, currentPageId), + app.state, + ids.filter((id) => (prop === 'isLocked' ? true : !app.getShape(id).isLocked)), () => ({ [prop]: !isAllToggled, }), diff --git a/packages/tldraw/src/state/commands/translateShapes/translateShapes.spec.ts b/packages/tldraw/src/state/commands/translateShapes/translateShapes.spec.ts index 9c31bab4f..e40825570 100644 --- a/packages/tldraw/src/state/commands/translateShapes/translateShapes.spec.ts +++ b/packages/tldraw/src/state/commands/translateShapes/translateShapes.spec.ts @@ -1,137 +1,137 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { TLDrawState } from '~state' -import { mockDocument, TLDrawStateUtils } from '~test' -import { ArrowShape, SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, SessionType, TDShapeType } from '~types' describe('Translate command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.nudge([1, 2]) - const currentState = state.state + const initialState = app.state + app.nudge([1, 2]) + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.selectAll() - state.nudge([1, 2]) + app.selectAll() + app.nudge([1, 2]) - expect(state.getShape('rect2').point).toEqual([101, 102]) + expect(app.getShape('rect2').point).toEqual([101, 102]) - state.undo() + app.undo() - expect(state.getShape('rect2').point).toEqual([100, 100]) + expect(app.getShape('rect2').point).toEqual([100, 100]) - state.redo() + app.redo() - expect(state.getShape('rect2').point).toEqual([101, 102]) + expect(app.getShape('rect2').point).toEqual([101, 102]) }) it('major nudges', () => { - state.selectAll() - state.nudge([1, 2], true) - expect(state.getShape('rect2').point).toEqual([110, 120]) + app.selectAll() + app.nudge([1, 2], true) + expect(app.getShape('rect2').point).toEqual([110, 120]) }) describe('when nudging shapes with bindings', () => { it('deleted bindings if nudging shape is bound to other shapes', () => { - state + app .resetDocument() .createShapes( { id: 'target1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], }, { - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200], } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const bindingId = state.getShape('arrow1').handles.start.bindingId! + const bindingId = app.getShape('arrow1').handles.start.bindingId! - state.select('arrow1').nudge([10, 10]) + app.select('arrow1').nudge([10, 10]) - expect(state.getBinding(bindingId)).toBeUndefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBeUndefined() + expect(app.getBinding(bindingId)).toBeUndefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBeUndefined() - state.undo() + app.undo() - expect(state.getBinding(bindingId)).toBeDefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + expect(app.getBinding(bindingId)).toBeDefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.redo() + app.redo() - expect(state.getBinding(bindingId)).toBeUndefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBeUndefined() + expect(app.getBinding(bindingId)).toBeUndefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBeUndefined() }) it('does not delete bindings if both bound and bound-to shapes are nudged', () => { - state + app .resetDocument() .createShapes( { id: 'target1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], }, { - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200], } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const bindingId = state.getShape('arrow1').handles.start.bindingId! + const bindingId = app.getShape('arrow1').handles.start.bindingId! - state.select('arrow1', 'target1').nudge([10, 10]) + app.select('arrow1', 'target1').nudge([10, 10]) - expect(state.getBinding(bindingId)).toBeDefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + expect(app.getBinding(bindingId)).toBeDefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.undo() + app.undo() - expect(state.getBinding(bindingId)).toBeDefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + expect(app.getBinding(bindingId)).toBeDefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) - state.redo() + app.redo() - expect(state.getBinding(bindingId)).toBeDefined() - expect(state.getShape('arrow1').handles.start.bindingId).toBe(bindingId) + expect(app.getBinding(bindingId)).toBeDefined() + expect(app.getShape('arrow1').handles.start.bindingId).toBe(bindingId) }) }) }) describe('When nudging groups', () => { it('nudges children instead', () => { - const state = new TLDrawState() + new TldrawTestApp() .loadDocument(mockDocument) .group(['rect1', 'rect2'], 'groupA') .nudge([1, 1]) - - new TLDrawStateUtils(state).expectShapesToBeAtPoints({ - rect1: [1, 1], - rect2: [101, 101], - }) + .expectShapesToBeAtPoints({ + rect1: [1, 1], + rect2: [101, 101], + }) }) }) diff --git a/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts b/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts index 9f858e78a..f8671af2f 100644 --- a/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts +++ b/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts @@ -1,16 +1,13 @@ import { Vec } from '@tldraw/vec' -import { TLDrawSnapshot, TLDrawCommand, PagePartial, Session } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawCommand, PagePartial } from '~types' +import type { TldrawApp } from '../../internal' -export function translateShapes( - data: TLDrawSnapshot, - ids: string[], - delta: number[] -): TLDrawCommand { - const { currentPageId } = data.appState +export function translateShapes(app: TldrawApp, ids: string[], delta: number[]): TldrawCommand { + const { currentPageId, selectedIds } = app // Clear session cache - Session.cache.selectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId) + app.rotationInfo.selectedIds = [...selectedIds] const before: PagePartial = { shapes: {}, @@ -22,13 +19,15 @@ export function translateShapes( bindings: {}, } - const idsToMutate = ids.flatMap((id) => { - const shape = TLDR.getShape(data, id, currentPageId) - return shape.children ? shape.children : shape.id - }) + const idsToMutate = ids + .flatMap((id) => { + const shape = app.getShape(id) + return shape.children ? shape.children : shape.id + }) + .filter((id) => !app.getShape(id).isLocked) const change = TLDR.mutateShapes( - data, + app.state, idsToMutate, (shape) => ({ point: Vec.round(Vec.add(shape.point, delta)), @@ -40,7 +39,7 @@ export function translateShapes( after.shapes = change.after // Delete bindings from nudged shapes, unless both bound and bound-to shapes are selected - const bindingsToDelete = TLDR.getBindings(data, currentPageId).filter( + const bindingsToDelete = TLDR.getBindings(app.state, currentPageId).filter( (binding) => ids.includes(binding.fromId) && !ids.includes(binding.toId) ) @@ -50,7 +49,7 @@ export function translateShapes( for (const id of [binding.toId, binding.fromId]) { // Let's also look at the bound shape... - const shape = TLDR.getShape(data, id, data.appState.currentPageId) + const shape = app.getShape(id) if (!shape.handles) continue diff --git a/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.spec.ts b/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.spec.ts index 3e7062154..50e40b76c 100644 --- a/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.spec.ts +++ b/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.spec.ts @@ -1,85 +1,84 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { GroupShape, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { GroupShape, TDShapeType } from '~types' describe('Ungroup command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() it('does, undoes and redoes command', () => { - state.loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA').select('groupA').ungroup() + app.loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA').select('groupA').ungroup() - expect(state.getShape('groupA')).toBeUndefined() - expect(state.getShape('rect1').parentId).toBe('page1') - expect(state.getShape('rect2').parentId).toBe('page1') + expect(app.getShape('groupA')).toBeUndefined() + expect(app.getShape('rect1').parentId).toBe('page1') + expect(app.getShape('rect2').parentId).toBe('page1') - state.undo() + app.undo() - expect(state.getShape('groupA')).toBeDefined() - expect(state.getShape('groupA').children).toStrictEqual(['rect1', 'rect2']) - expect(state.getShape('rect1').parentId).toBe('groupA') - expect(state.getShape('rect2').parentId).toBe('groupA') + expect(app.getShape('groupA')).toBeDefined() + expect(app.getShape('groupA').children).toStrictEqual(['rect1', 'rect2']) + expect(app.getShape('rect1').parentId).toBe('groupA') + expect(app.getShape('rect2').parentId).toBe('groupA') - state.redo() + app.redo() - expect(state.getShape('groupA')).toBeUndefined() - expect(state.getShape('rect1').parentId).toBe('page1') - expect(state.getShape('rect2').parentId).toBe('page1') + expect(app.getShape('groupA')).toBeUndefined() + expect(app.getShape('rect1').parentId).toBe('page1') + expect(app.getShape('rect2').parentId).toBe('page1') }) describe('When ungrouping', () => { it('Ungroups shapes on any page', () => { - state + app .loadDocument(mockDocument) .group(['rect1', 'rect2'], 'groupA') .createPage('page2') .ungroup(['groupA'], 'page1') - expect(state.getShape('groupA', 'page1')).toBeUndefined() - state.undo() - expect(state.getShape('groupA', 'page1')).toBeDefined() + expect(app.getShape('groupA', 'page1')).toBeUndefined() + app.undo() + expect(app.getShape('groupA', 'page1')).toBeDefined() }) it('Ungroups multiple selected groups', () => { - state + app .loadDocument(mockDocument) .createShapes({ id: 'rect4', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, }) .group(['rect1', 'rect2'], 'groupA') .group(['rect3', 'rect4'], 'groupB') .selectAll() .ungroup() - expect(state.getShape('groupA', 'page1')).toBeUndefined() - expect(state.getShape('groupB', 'page1')).toBeUndefined() + expect(app.getShape('groupA', 'page1')).toBeUndefined() + expect(app.getShape('groupB', 'page1')).toBeUndefined() }) it('Does not ungroup if a group shape is not selected', () => { - state.loadDocument(mockDocument).select('rect1') - const before = state.state - state.group() + app.loadDocument(mockDocument).select('rect1') + const before = app.state + app.group() // State should not have changed - expect(state.state).toStrictEqual(before) + expect(app.state).toStrictEqual(before) }) it('Correctly selects children after ungrouping', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) @@ -88,42 +87,42 @@ describe('Ungroup command', () => { .ungroup() // State should not have changed - expect(state.selectedIds).toStrictEqual(['rect3', 'rect1', 'rect2']) + expect(app.selectedIds).toStrictEqual(['rect3', 'rect1', 'rect2']) }) it('Reparents shapes to the page at the correct childIndex', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 1, }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 2, }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, childIndex: 3, } ) .group(['rect1', 'rect2'], 'groupA') - const { childIndex } = state.getShape('groupA') + const { childIndex } = app.getShape('groupA') expect(childIndex).toBe(1) - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect3').childIndex).toBe(3) + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect3').childIndex).toBe(3) - state.ungroup() + app.ungroup() - expect(state.getShape('rect1').childIndex).toBe(1) - expect(state.getShape('rect2').childIndex).toBe(2) - expect(state.getShape('rect3').childIndex).toBe(3) + expect(app.getShape('rect1').childIndex).toBe(1) + expect(app.getShape('rect2').childIndex).toBe(2) + expect(app.getShape('rect3').childIndex).toBe(3) }) it.todo('Deletes any bindings to the group') }) diff --git a/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.ts b/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.ts index f4ebb7414..54deb45ec 100644 --- a/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.ts +++ b/packages/tldraw/src/state/commands/ungroupShapes/ungroupShapes.ts @@ -1,110 +1,113 @@ -import type { GroupShape, TLDrawBinding, TLDrawShape } from '~types' -import type { TLDrawSnapshot, TLDrawCommand } from '~types' import { TLDR } from '~state/TLDR' +import type { GroupShape, TDBinding, TDShape } from '~types' +import type { TldrawCommand } from '~types' import type { Patch } from 'rko' +import type { TldrawApp } from '../../internal' export function ungroupShapes( - data: TLDrawSnapshot, + app: TldrawApp, selectedIds: string[], groupShapes: GroupShape[], pageId: string -): TLDrawCommand | undefined { - const beforeShapes: Record> = {} - const afterShapes: Record> = {} +): TldrawCommand | undefined { + const { bindings } = app - const beforeBindings: Record> = {} - const afterBindings: Record> = {} + const beforeShapes: Record> = {} + const afterShapes: Record> = {} + + const beforeBindings: Record> = {} + const afterBindings: Record> = {} const beforeSelectedIds = selectedIds const afterSelectedIds = selectedIds.filter((id) => !groupShapes.find((shape) => shape.id === id)) // The group shape - groupShapes.forEach((groupShape) => { - const shapesToReparent: TLDrawShape[] = [] - const deletedGroupIds: string[] = [] + groupShapes + .filter((shape) => !shape.isLocked) + .forEach((groupShape) => { + const shapesToReparent: TDShape[] = [] + const deletedGroupIds: string[] = [] - // Remove the group shape in the next state - beforeShapes[groupShape.id] = groupShape - afterShapes[groupShape.id] = undefined + // Remove the group shape in the next state + beforeShapes[groupShape.id] = groupShape + afterShapes[groupShape.id] = undefined - // Select its children in the next state - groupShape.children.forEach((id) => { - afterSelectedIds.push(id) - const shape = TLDR.getShape(data, id, pageId) - shapesToReparent.push(shape) - }) + // Select its children in the next state + groupShape.children.forEach((id) => { + afterSelectedIds.push(id) + const shape = app.getShape(id, pageId) + shapesToReparent.push(shape) + }) - // We'll start placing the shapes at this childIndex - const startingChildIndex = groupShape.childIndex + // We'll start placing the shapes at this childIndex + const startingChildIndex = groupShape.childIndex - // And we'll need to fit them under this child index - const endingChildIndex = TLDR.getChildIndexAbove(data, groupShape.id, pageId) + // And we'll need to fit them under this child index + const endingChildIndex = TLDR.getChildIndexAbove(app.state, groupShape.id, pageId) - const step = (endingChildIndex - startingChildIndex) / shapesToReparent.length + const step = (endingChildIndex - startingChildIndex) / shapesToReparent.length - // An array of shapes in order by their child index - const sortedShapes = shapesToReparent.sort((a, b) => a.childIndex - b.childIndex) + // An array of shapes in order by their child index + const sortedShapes = shapesToReparent.sort((a, b) => a.childIndex - b.childIndex) - // Reparent shapes to the page - sortedShapes.forEach((shape, index) => { - beforeShapes[shape.id] = { - parentId: shape.parentId, - childIndex: shape.childIndex, - } + // Reparent shapes to the page + sortedShapes.forEach((shape, index) => { + beforeShapes[shape.id] = { + parentId: shape.parentId, + childIndex: shape.childIndex, + } - afterShapes[shape.id] = { - parentId: pageId, - childIndex: startingChildIndex + step * index, - } - }) - - const page = TLDR.getPage(data, pageId) - - // We also need to delete bindings that reference the deleted shapes - Object.values(page.bindings) - .filter((binding) => binding.toId === groupShape.id || binding.fromId === groupShape.id) - .forEach((binding) => { - for (const id of [binding.toId, binding.fromId]) { - // If the binding references the deleted group... - if (afterShapes[id] === undefined) { - // Delete the binding - beforeBindings[binding.id] = binding - afterBindings[binding.id] = undefined - - // Let's also look each the bound shape... - const shape = TLDR.getShape(data, id, pageId) - - // If the bound shape has a handle that references the deleted binding... - if (shape.handles) { - Object.values(shape.handles) - .filter((handle) => handle.bindingId === binding.id) - .forEach((handle) => { - // Save the binding reference in the before patch - beforeShapes[id] = { - ...beforeShapes[id], - handles: { - ...beforeShapes[id]?.handles, - [handle.id]: { bindingId: binding.id }, - }, - } - - // Unless we're currently deleting the shape, remove the - // binding reference from the after patch - if (!deletedGroupIds.includes(id)) { - afterShapes[id] = { - ...afterShapes[id], - handles: { - ...afterShapes[id]?.handles, - [handle.id]: { bindingId: undefined }, - }, - } - } - }) - } - } + afterShapes[shape.id] = { + parentId: pageId, + childIndex: startingChildIndex + step * index, } }) - }) + + // We also need to delete bindings that reference the deleted shapes + bindings + .filter((binding) => binding.toId === groupShape.id || binding.fromId === groupShape.id) + .forEach((binding) => { + for (const id of [binding.toId, binding.fromId]) { + // If the binding references the deleted group... + if (afterShapes[id] === undefined) { + // Delete the binding + beforeBindings[binding.id] = binding + afterBindings[binding.id] = undefined + + // Let's also look each the bound shape... + const shape = app.getShape(id, pageId) + + // If the bound shape has a handle that references the deleted binding... + if (shape.handles) { + Object.values(shape.handles) + .filter((handle) => handle.bindingId === binding.id) + .forEach((handle) => { + // Save the binding reference in the before patch + beforeShapes[id] = { + ...beforeShapes[id], + handles: { + ...beforeShapes[id]?.handles, + [handle.id]: { bindingId: binding.id }, + }, + } + + // Unless we're currently deleting the shape, remove the + // binding reference from the after patch + if (!deletedGroupIds.includes(id)) { + afterShapes[id] = { + ...afterShapes[id], + handles: { + ...afterShapes[id]?.handles, + [handle.id]: { bindingId: undefined }, + }, + } + } + }) + } + } + } + }) + }) return { id: 'ungroup', diff --git a/packages/tldraw/src/state/commands/updateShapes/updateShapes.spec.ts b/packages/tldraw/src/state/commands/updateShapes/updateShapes.spec.ts index c51ab8f07..3258e9913 100644 --- a/packages/tldraw/src/state/commands/updateShapes/updateShapes.spec.ts +++ b/packages/tldraw/src/state/commands/updateShapes/updateShapes.spec.ts @@ -1,34 +1,33 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' describe('Update command', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() beforeEach(() => { - state.loadDocument(mockDocument) + app.loadDocument(mockDocument) }) describe('when no shape is selected', () => { it('does nothing', () => { - const initialState = state.state - state.updateShapes() - const currentState = state.state + const initialState = app.state + app.updateShapes() + const currentState = app.state expect(currentState).toEqual(initialState) }) }) it('does, undoes and redoes command', () => { - state.updateShapes({ id: 'rect1', point: [100, 100] }) + app.updateShapes({ id: 'rect1', point: [100, 100] }) - expect(state.getShape('rect1').point).toStrictEqual([100, 100]) + expect(app.getShape('rect1').point).toStrictEqual([100, 100]) - state.undo() + app.undo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) - state.redo() + app.redo() - expect(state.getShape('rect1').point).toStrictEqual([100, 100]) + expect(app.getShape('rect1').point).toStrictEqual([100, 100]) }) }) diff --git a/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts b/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts index 129e4bdc7..0c9141f05 100644 --- a/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts +++ b/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts @@ -1,41 +1,38 @@ -import type { TLDrawSnapshot, TLDrawCommand, PagePartial, TLDrawShape } from '~types' +import type { TldrawCommand, TDShape } from '~types' import { TLDR } from '~state/TLDR' +import type { TldrawApp } from '../../internal' export function update( - data: TLDrawSnapshot, - updates: ({ id: string } & Partial)[], + app: TldrawApp, + updates: ({ id: string } & Partial)[], pageId: string -): TLDrawCommand { +): TldrawCommand { const ids = updates.map((update) => update.id) - const before: PagePartial = { - shapes: {}, - bindings: {}, - } - - const after: PagePartial = { - shapes: {}, - bindings: {}, - } - - const change = TLDR.mutateShapes(data, ids, (_shape, i) => updates[i], pageId) - - before.shapes = change.before - after.shapes = change.after + const change = TLDR.mutateShapes( + app.state, + ids.filter((id) => !app.getShape(id, pageId).isLocked), + (_shape, i) => updates[i], + pageId + ) return { id: 'update', before: { document: { pages: { - [pageId]: before, + [pageId]: { + shapes: change.before, + }, }, }, }, after: { document: { pages: { - [pageId]: after, + [pageId]: { + shapes: change.after, + }, }, }, }, diff --git a/packages/tldraw/src/state/constants.ts b/packages/tldraw/src/state/constants.ts deleted file mode 100644 index e41477a07..000000000 --- a/packages/tldraw/src/state/constants.ts +++ /dev/null @@ -1 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/packages/tldraw/src/state/data/filesystem.ts b/packages/tldraw/src/state/data/filesystem.ts index f028567ba..e48e9720d 100644 --- a/packages/tldraw/src/state/data/filesystem.ts +++ b/packages/tldraw/src/state/data/filesystem.ts @@ -1,4 +1,4 @@ -import type { TLDrawDocument, TLDrawFile } from '~types' +import type { TDDocument, TDFile } from '~types' import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access' import { get as getFromIdb, set as setToIdb } from 'idb-keyval' @@ -12,21 +12,18 @@ const checkPermissions = async (handle: FileSystemHandle) => { } export async function loadFileHandle() { - const fileHandle = await getFromIdb(`tldraw_file_handle_${window.location.origin}`) + const fileHandle = await getFromIdb(`Tldraw_file_handle_${window.location.origin}`) if (!fileHandle) return null return fileHandle } export async function saveFileHandle(fileHandle: FileSystemHandle | null) { - return setToIdb(`tldraw_file_handle_${window.location.origin}`, fileHandle) + return setToIdb(`Tldraw_file_handle_${window.location.origin}`, fileHandle) } -export async function saveToFileSystem( - document: TLDrawDocument, - fileHandle: FileSystemHandle | null -) { +export async function saveToFileSystem(document: TDDocument, fileHandle: FileSystemHandle | null) { // Create the saved file data - const file: TLDrawFile = { + const file: TDFile = { name: document.name || 'New Document', fileHandle: fileHandle ?? null, document, @@ -38,7 +35,7 @@ export async function saveToFileSystem( // Create blob const blob = new Blob([json], { - type: 'application/vnd.tldraw+json', + type: 'application/vnd.Tldraw+json', }) if (fileHandle) { @@ -51,7 +48,7 @@ export async function saveToFileSystem( blob, { fileName: `${file.name}.tldr`, - description: 'TLDraw File', + description: 'Tldraw File', extensions: [`.tldr`], }, fileHandle @@ -65,11 +62,11 @@ export async function saveToFileSystem( export async function openFromFileSystem(): Promise { // Get the blob const blob = await fileOpen({ - description: 'TLDraw File', + description: 'Tldraw File', extensions: [`.tldr`], multiple: false, }) @@ -88,7 +85,7 @@ export async function openFromFileSystem(): Promise { it('migrates a document without a version', () => { - new TLDrawState().loadDocument(oldDoc as unknown as TLDrawDocument) + new TldrawApp().loadDocument(oldDoc as unknown as TDDocument) }) it('migrates a document with an older version', () => { - const state = new TLDrawState().loadDocument(oldDoc2 as unknown as TLDrawDocument) - expect(state.getShape('d7ab0a49-3cb3-43ae-3d83-f5cf2f4a510a').style.color).toBe('black') + const app = new TldrawApp().loadDocument(oldDoc2 as unknown as TDDocument) + expect(app.getShape('d7ab0a49-3cb3-43ae-3d83-f5cf2f4a510a').style.color).toBe('black') }) }) diff --git a/packages/tldraw/src/state/data/migrate.ts b/packages/tldraw/src/state/data/migrate.ts index 8bfdd47bd..041ea5487 100644 --- a/packages/tldraw/src/state/data/migrate.ts +++ b/packages/tldraw/src/state/data/migrate.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { Decoration, TLDrawDocument, TLDrawShapeType } from '~types' +import { Decoration, TDDocument, TDShapeType } from '~types' -export function migrate(document: TLDrawDocument, newVersion: number): TLDrawDocument { +export function migrate(document: TDDocument, newVersion: number): TDDocument { const { version = 0 } = document if (version === newVersion) return document @@ -22,7 +22,7 @@ export function migrate(document: TLDrawDocument, newVersion: number): TLDrawDoc } }) - if (shape.type === TLDrawShapeType.Arrow) { + if (shape.type === TDShapeType.Arrow) { if (shape.decorations) { Object.entries(shape.decorations).forEach(([id, decoration]) => { if ((decoration as unknown) === 'Arrow') { diff --git a/packages/tldraw/src/state/index.ts b/packages/tldraw/src/state/index.ts index d6f77550e..368f0e4c9 100644 --- a/packages/tldraw/src/state/index.ts +++ b/packages/tldraw/src/state/index.ts @@ -1 +1,3 @@ -export * from './TLDrawState' +import './internal' + +export * from './TldrawApp' diff --git a/packages/tldraw/src/state/internal.ts b/packages/tldraw/src/state/internal.ts new file mode 100644 index 000000000..f89f9c2a4 --- /dev/null +++ b/packages/tldraw/src/state/internal.ts @@ -0,0 +1,4 @@ +export * from './TldrawApp' +export * from './sessions' +export * from './commands' +export * from './tools' diff --git a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.spec.ts b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.spec.ts index 09f038075..425236d8d 100644 --- a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.spec.ts +++ b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.spec.ts @@ -1,141 +1,152 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { ArrowShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { ArrowShape, SessionType, TDShapeType, TDStatus } from '~types' describe('Arrow session', () => { - const restoreDoc = new TLDrawState() + const restoreDoc = new TldrawTestApp() .loadDocument(mockDocument) .selectAll() .delete() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] } + { type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] } ).document it('begins, updateSession', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - const binding = state.bindings[0] + const binding = app.bindings[0] expect(binding).toBeTruthy() expect(binding.fromId).toBe('arrow1') expect(binding.toId).toBe('target1') expect(binding.handleId).toBe('start') - expect(state.appState.status).toBe(TLDrawStatus.Idle) - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) + expect(app.appState.status).toBe(TDStatus.Idle) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) - state.undo() + app.undo() - expect(state.bindings[0]).toBe(undefined) - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined) + expect(app.bindings[0]).toBe(undefined) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(undefined) - state.redo() + app.redo() - expect(state.bindings[0]).toBeTruthy() - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) + expect(app.bindings[0]).toBeTruthy() + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) }) it('cancels session', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .cancelSession() - expect(state.bindings[0]).toBe(undefined) - expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined) + expect(app.bindings[0]).toBe(undefined) + expect(app.getShape('arrow1').handles?.start.bindingId).toBe(undefined) }) describe('arrow binding', () => { it('points to the center', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) - expect(state.bindings[0].point).toStrictEqual([0.5, 0.5]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) + expect(app.bindings[0].point).toStrictEqual([0.5, 0.5]) }) it('Snaps to the center', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([55, 55]) - expect(state.bindings[0].point).toStrictEqual([0.5, 0.5]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([55, 55]) + expect(app.bindings[0].point).toStrictEqual([0.5, 0.5]) }) it('Binds at the bottom left', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([124, -24]) - expect(state.bindings[0].point).toStrictEqual([1, 0]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([124, -24]) + expect(app.bindings[0].point).toStrictEqual([1, 0]) }) it('Cancels the bind when off of the expanded bounds', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([133, 133]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([133, 133]) - expect(state.bindings[0]).toBe(undefined) + expect(app.bindings[0]).toBe(undefined) }) it('binds on the inside of a shape while alt is held', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([91, 9]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([91, 9]) - expect(state.bindings[0].point).toStrictEqual([0.71, 0.11]) + expect(app.bindings[0].point).toStrictEqual([0.71, 0.11]) - state.updateSession([91, 9], false, true, false) + app.movePointer({ x: 91, y: 9, altKey: true }) }) it('snaps to the inside center when the point is close to the center', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([91, 9], false, true, false) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer({ x: 91, y: 9, altKey: true }) - expect(state.bindings[0].point).toStrictEqual([0.78, 0.22]) + expect(app.bindings[0].point).toStrictEqual([0.78, 0.22]) }) it('ignores binding when meta is held', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(restoreDoc) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([55, 45], false, false, true) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer({ x: 55, y: 45, ctrlKey: true }) - expect(state.bindings.length).toBe(0) + expect(app.bindings.length).toBe(0) }) }) describe('when dragging a bound shape', () => { it('updates the arrow', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() - state.loadDocument(restoreDoc) - // Select the arrow and begin a session on the handle's start handle - state.select('arrow1').startSession(SessionType.Arrow, [200, 200], 'start') + app + .loadDocument(restoreDoc) + // Select the arrow and begin a session on the handle's start handle + .movePointer([200, 200]) + .select('arrow1') + .startSession(SessionType.Arrow, 'arrow1', 'start') // Move to [50,50] - state.updateSession([50, 50]) + app.movePointer([50, 50]) // Both handles will keep the same screen positions, but their points will have changed. - expect(state.getShape('arrow1').point).toStrictEqual([116, 116]) - expect(state.getShape('arrow1').handles.start.point).toStrictEqual([0, 0]) - expect(state.getShape('arrow1').handles.end.point).toStrictEqual([85, 85]) + expect(app.getShape('arrow1').point).toStrictEqual([116, 116]) + expect(app.getShape('arrow1').handles.start.point).toStrictEqual([0, 0]) + expect(app.getShape('arrow1').handles.end.point).toStrictEqual([85, 85]) }) it.todo('updates the arrow when bound on both sides') @@ -146,84 +157,81 @@ describe('Arrow session', () => { describe('When creating with an arrow session', () => { it('Deletes the shape on undo', () => { - const state = new TLDrawState() - .createShapes({ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }) + const app = new TldrawTestApp() + .createShapes({ type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] }) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start', true) - .updateSession([55, 45]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start', true) + .movePointer([55, 45]) .completeSession() .undo() - expect(state.getShape('arrow1')).toBe(undefined) + expect(app.getShape('arrow1')).toBe(undefined) }) it("Doesn't corrupt a shape after undoing", () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, - { type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [450, 250] } + { type: TDShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, + { type: TDShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] } ) - .select('arrow1') - .startSession(SessionType.Arrow, [250, 250], 'start', true) - .updateSession([55, 45]) - .completeSession() + .selectTool(TDShapeType.Arrow) + .pointShape('rect1', { x: 250, y: 250 }) + .movePointer([450, 250]) + .stopPointing() - expect(state.bindings.length).toBe(2) + expect(app.bindings.length).toBe(2) - state - .undo() - .select('rect1') - .startSession(SessionType.Translate, [250, 250]) - .updateSession([275, 275]) - .completeSession() + app.undo() - expect(state.bindings.length).toBe(0) + expect(app.bindings.length).toBe(0) + + app.select('rect1').pointShape('rect1', [250, 250]).movePointer([275, 275]).completeSession() + + expect(app.bindings.length).toBe(0) }) it('Creates a start binding if possible', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, - { type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [250, 250] } + { type: TDShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, + { type: TDShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] } ) - .select('arrow1') - .startSession(SessionType.Arrow, [250, 250], 'end', true) - .updateSession([450, 250]) + .selectTool(TDShapeType.Arrow) + .pointShape('rect1', { x: 250, y: 250 }) + .movePointer([450, 250]) .completeSession() - const arrow = state.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape + const arrow = app.shapes.find((shape) => shape.type === TDShapeType.Arrow) as ArrowShape expect(arrow).toBeTruthy() - - expect(state.bindings.length).toBe(2) - + expect(app.bindings.length).toBe(2) expect(arrow.handles.start.bindingId).not.toBe(undefined) expect(arrow.handles.end.bindingId).not.toBe(undefined) }) it('Removes a binding when dragged away', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .createShapes( - { type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, - { type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] }, - { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [250, 250] } + { type: TDShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] }, + { type: TDShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] }, + { type: TDShapeType.Arrow, id: 'arrow1', point: [250, 250] } ) .select('arrow1') - .startSession(SessionType.Arrow, [250, 250], 'end', true) - .updateSession([450, 250]) + .movePointer([250, 250]) + .startSession(SessionType.Arrow, 'arrow1', 'end', true) + .movePointer([450, 250]) .completeSession() .select('arrow1') - .startSession(SessionType.Arrow, [250, 250], 'start', false) - .updateSession([0, 0]) + .startSession(SessionType.Arrow, 'arrow1', 'start', false) + .movePointer([0, 0]) .completeSession() - const arrow = state.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape + const arrow = app.shapes.find((shape) => shape.type === TDShapeType.Arrow) as ArrowShape expect(arrow).toBeTruthy() - expect(state.bindings.length).toBe(1) + expect(app.bindings.length).toBe(1) expect(arrow.handles.start.point).toStrictEqual([0, 0]) expect(arrow.handles.start.bindingId).toBe(undefined) diff --git a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts index 5cf2537b8..a0b806e78 100644 --- a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts +++ b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts @@ -1,74 +1,57 @@ import { ArrowBinding, ArrowShape, - TLDrawShape, - TLDrawBinding, - TLDrawSnapshot, - Session, - TLDrawStatus, + TDShape, + TDBinding, + TDStatus, SessionType, - TLDrawShapeType, + TDShapeType, + TldrawPatch, + TldrawCommand, } from '~types' import { Vec } from '@tldraw/vec' -import { Utils, TLBounds } from '@tldraw/core' +import { Utils } from '@tldraw/core' import { TLDR } from '~state/TLDR' import { BINDING_DISTANCE } from '~constants' import { shapeUtils } from '~state/shapes' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' -export class ArrowSession extends Session { - static type = SessionType.Arrow - status = TLDrawStatus.TranslatingHandle +export class ArrowSession extends BaseSession { + type = SessionType.Arrow + status = TDStatus.TranslatingHandle newStartBindingId = Utils.uniqueId() draggedBindingId = Utils.uniqueId() didBind = false - delta = [0, 0] - offset = [0, 0] - origin: number[] - topLeft: number[] initialShape: ArrowShape handleId: 'start' | 'end' bindableShapeIds: string[] - initialBinding?: TLDrawBinding + initialBinding?: TDBinding startBindingShapeId?: string isCreate: boolean - constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - point: number[], - handleId: 'start' | 'end', - isCreate = false - ) { - super(viewport) - + constructor(app: TldrawApp, shapeId: string, handleId: 'start' | 'end', isCreate = false) { + super(app) this.isCreate = isCreate - - const { currentPageId } = data.appState - const page = data.document.pages[currentPageId] - const pageState = data.document.pageStates[currentPageId] - - const shapeId = pageState.selectedIds[0] - - this.origin = point - + const { currentPageId } = app.state.appState + const page = app.state.document.pages[currentPageId] this.handleId = handleId - this.initialShape = page.shapes[shapeId] as ArrowShape - - this.topLeft = this.initialShape.point - - this.bindableShapeIds = TLDR.getBindableShapeIds(data).filter( + this.bindableShapeIds = TLDR.getBindableShapeIds(app.state).filter( (id) => !(id === this.initialShape.id || id === this.initialShape.parentId) ) + const { originPoint } = this.app + if (this.isCreate) { // If we're creating a new shape, should we bind its first point? // The method may return undefined, which is correct if there is no // bindable shape under the pointer. - this.startBindingShapeId = this.bindableShapeIds .map((id) => page.shapes[id]) - .find((shape) => Utils.pointInBounds(point, TLDR.getShapeUtils(shape).getBounds(shape)))?.id + .find((shape) => + Utils.pointInBounds(originPoint, TLDR.getShapeUtil(shape).getBounds(shape)) + )?.id } else { // If we're editing an existing line, is there a binding already // for the dragging handle? @@ -83,20 +66,15 @@ export class ArrowSession extends Session { } } - start = () => void null + start = (): TldrawPatch | undefined => void null - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { + update = (): TldrawPatch | undefined => { const { initialShape } = this + const { currentPoint, shiftKey, altKey, metaKey } = this.app - const page = TLDR.getPage(data, data.appState.currentPageId) + const shape = this.app.getShape(initialShape.id) - const shape = TLDR.getShape(data, initialShape.id, data.appState.currentPageId) + if (shape.isLocked) return const handles = shape.handles @@ -108,7 +86,7 @@ export class ArrowSession extends Session { // First update the handle's next point - const delta = Vec.sub(point, handles[handleId].point) + const delta = Vec.sub(currentPoint, handles[handleId].point) const handle = { ...handles[handleId], @@ -116,7 +94,7 @@ export class ArrowSession extends Session { bindingId: undefined, } - const utils = shapeUtils[TLDrawShapeType.Arrow] + const utils = shapeUtils[TDShapeType.Arrow] const change = utils.onHandleChange?.( shape, @@ -133,7 +111,7 @@ export class ArrowSession extends Session { // before. If it does change, we'll redefine this later on. And if we've // made it this far, the shape should be a new object reference that // incorporates the changes we've made due to the handle movement. - const next: { shape: ArrowShape; bindings: Record } = { + const next: { shape: ArrowShape; bindings: Record } = { shape: Utils.deepMerge(shape, change), bindings: {}, } @@ -149,9 +127,9 @@ export class ArrowSession extends Session { if (this.startBindingShapeId) { let startBinding: ArrowBinding | undefined - const target = page.shapes[this.startBindingShapeId] + const target = this.app.page.shapes[this.startBindingShapeId] - const targetUtils = TLDR.getShapeUtils(target) + const targetUtils = TLDR.getShapeUtil(target) if (!metaKey) { const center = targetUtils.getCenter(target) @@ -185,11 +163,11 @@ export class ArrowSession extends Session { }, } - const target = page.shapes[this.startBindingShapeId] + const target = this.app.page.shapes[this.startBindingShapeId] - const targetUtils = TLDR.getShapeUtils(target) + const targetUtils = TLDR.getShapeUtil(target) - const arrowChange = TLDR.getShapeUtils(next.shape.type).onBindingChange?.( + const arrowChange = TLDR.getShapeUtil(next.shape.type).onBindingChange?.( next.shape, startBinding, target, @@ -203,7 +181,7 @@ export class ArrowSession extends Session { } else { this.didBind = this.didBind || false - if (page.bindings[this.newStartBindingId]) { + if (this.app.page.bindings[this.newStartBindingId]) { next.bindings[this.newStartBindingId] = undefined } @@ -230,7 +208,7 @@ export class ArrowSession extends Session { const rayPoint = Vec.add(handle.point, next.shape.point) const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin)) - const targets = this.bindableShapeIds.map((id) => page.shapes[id]) + const targets = this.bindableShapeIds.map((id) => this.app.page.shapes[id]) for (const target of targets) { draggedBinding = this.findBindingPoint( @@ -261,11 +239,11 @@ export class ArrowSession extends Session { }, } - const target = page.shapes[draggedBinding.toId] + const target = this.app.page.shapes[draggedBinding.toId] - const targetUtils = TLDR.getShapeUtils(target) + const targetUtils = TLDR.getShapeUtil(target) - const utils = shapeUtils[TLDrawShapeType.Arrow] + const utils = shapeUtils[TDShapeType.Arrow] const arrowChange = utils.onBindingChange( next.shape, @@ -302,7 +280,7 @@ export class ArrowSession extends Session { return { document: { pages: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { shapes: { [shape.id]: next.shape, }, @@ -310,7 +288,7 @@ export class ArrowSession extends Session { }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { bindingId: next.shape.handles[handleId].bindingId, }, }, @@ -318,10 +296,10 @@ export class ArrowSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { + cancel = (): TldrawPatch | undefined => { const { initialShape, initialBinding, newStartBindingId, draggedBindingId } = this - const afterBindings: Record = {} + const afterBindings: Record = {} afterBindings[draggedBindingId] = undefined @@ -336,7 +314,7 @@ export class ArrowSession extends Session { return { document: { pages: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { shapes: { [initialShape.id]: this.isCreate ? undefined : initialShape, }, @@ -344,7 +322,7 @@ export class ArrowSession extends Session { }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { selectedIds: this.isCreate ? [] : [initialShape.id], bindingId: undefined, hoveredId: undefined, @@ -355,16 +333,14 @@ export class ArrowSession extends Session { } } - complete = (data: TLDrawSnapshot) => { + complete = (): TldrawPatch | TldrawCommand | undefined => { const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this - const page = TLDR.getPage(data, data.appState.currentPageId) + const beforeBindings: Partial> = {} - const beforeBindings: Partial> = {} + const afterBindings: Partial> = {} - const afterBindings: Partial> = {} - - let afterShape = page.shapes[initialShape.id] as ArrowShape + let afterShape = this.app.page.shapes[initialShape.id] as ArrowShape const currentBindingId = afterShape.handles[handleId].bindingId @@ -375,12 +351,12 @@ export class ArrowSession extends Session { if (currentBindingId) { beforeBindings[currentBindingId] = undefined - afterBindings[currentBindingId] = page.bindings[currentBindingId] + afterBindings[currentBindingId] = this.app.page.bindings[currentBindingId] } if (startBindingShapeId) { beforeBindings[newStartBindingId] = undefined - afterBindings[newStartBindingId] = page.bindings[newStartBindingId] + afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId] } afterShape = TLDR.onSessionComplete(afterShape) @@ -390,7 +366,7 @@ export class ArrowSession extends Session { before: { document: { pages: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { shapes: { [initialShape.id]: this.isCreate ? undefined : initialShape, }, @@ -398,7 +374,7 @@ export class ArrowSession extends Session { }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { selectedIds: this.isCreate ? [] : [initialShape.id], bindingId: undefined, hoveredId: undefined, @@ -410,7 +386,7 @@ export class ArrowSession extends Session { after: { document: { pages: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { shapes: { [initialShape.id]: afterShape, }, @@ -418,7 +394,7 @@ export class ArrowSession extends Session { }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { selectedIds: [initialShape.id], bindingId: undefined, hoveredId: undefined, @@ -432,7 +408,7 @@ export class ArrowSession extends Session { private findBindingPoint = ( shape: ArrowShape, - target: TLDrawShape, + target: TDShape, handleId: 'start' | 'end', bindingId: string, point: number[], @@ -440,7 +416,7 @@ export class ArrowSession extends Session { direction: number[], bindAnywhere: boolean ) => { - const util = TLDR.getShapeUtils(target.type) + const util = TLDR.getShapeUtil(target.type) const bindingPoint = util.getBindingPoint( target, diff --git a/packages/tldraw/src/state/sessions/BaseSession.ts b/packages/tldraw/src/state/sessions/BaseSession.ts new file mode 100644 index 000000000..7dbc92a4d --- /dev/null +++ b/packages/tldraw/src/state/sessions/BaseSession.ts @@ -0,0 +1,16 @@ +import type { SessionType, TldrawCommand, TldrawPatch } from '~types' +import type { TldrawApp } from '../internal' + +export abstract class BaseSession { + abstract type: SessionType + + constructor(public app: TldrawApp) {} + + abstract start: () => TldrawPatch | undefined + + abstract update: () => TldrawPatch | undefined + + abstract complete: () => TldrawPatch | TldrawCommand | undefined + + abstract cancel: () => TldrawPatch | undefined +} diff --git a/packages/tldraw/src/state/sessions/BrushSession/BrushSession.spec.ts b/packages/tldraw/src/state/sessions/BrushSession/BrushSession.spec.ts index 26cedc89c..b0af19c06 100644 --- a/packages/tldraw/src/state/sessions/BrushSession/BrushSession.spec.ts +++ b/packages/tldraw/src/state/sessions/BrushSession/BrushSession.spec.ts @@ -1,61 +1,65 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { SessionType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDStatus } from '~types' describe('Brush session', () => { it('begins, updateSession', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectNone() - .startSession(SessionType.Brush, [-10, -10]) - .updateSession([10, 10]) + .movePointer([-10, -10]) + .startSession(SessionType.Brush) + .movePointer([10, 10]) .completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) - expect(state.selectedIds.length).toBe(1) + expect(app.status).toBe(TDStatus.Idle) + expect(app.selectedIds.length).toBe(1) }) it('selects multiple shapes', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectNone() - .startSession(SessionType.Brush, [-10, -10]) - .updateSession([110, 110]) + .movePointer([-10, -10]) + .startSession(SessionType.Brush) + .movePointer([110, 110]) .completeSession() - expect(state.selectedIds.length).toBe(3) + expect(app.selectedIds.length).toBe(3) }) - it('does not de-select original shapes', () => { - const state = new TLDrawState() + it('does not de-select original shapes when shift selecting', () => { + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectNone() .select('rect1') - .startSession(SessionType.Brush, [300, 300]) - .updateSession([301, 301]) + .pointCanvas({ x: 300, y: 300, shiftKey: true }) + .startSession(SessionType.Brush) + .movePointer({ x: 301, y: 301, shiftKey: true }) .completeSession() - expect(state.selectedIds.length).toBe(1) + expect(app.selectedIds.length).toBe(1) }) - // it('does not select hidden shapes', () => { - // const state = new TLDrawState() - // .loadDocument(mockDocument) - // .selectNone() - // .toggleHidden(['rect1']) - // .selectNone() - // .startSession(SessionType.Brush, [-10, -10]) - // .updateSession([10, 10]) - // .completeSession() - // }) + it('does not select locked shapes', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectNone() + .toggleLocked(['rect1']) + .selectNone() + .pointCanvas({ x: -10, y: -10, shiftKey: true }) + .movePointer([-10, -10]) + .completeSession() + expect(app.selectedIds.length).toBe(0) + }) it('when command is held, require the entire shape to be selected', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectNone() .loadDocument(mockDocument) .selectNone() - .startSession(SessionType.Brush, [-10, -10]) - .updateSession([10, 10], false, false, true) + .movePointer([-10, -10]) + .startSession(SessionType.Brush) + .movePointer({ x: 10, y: 10, shiftKey: false, altKey: false, ctrlKey: true }) .completeSession() - expect(state.selectedIds.length).toBe(0) + expect(app.selectedIds.length).toBe(0) }) }) diff --git a/packages/tldraw/src/state/sessions/BrushSession/BrushSession.ts b/packages/tldraw/src/state/sessions/BrushSession/BrushSession.ts index 1d8c403bc..86215df47 100644 --- a/packages/tldraw/src/state/sessions/BrushSession/BrushSession.ts +++ b/packages/tldraw/src/state/sessions/BrushSession/BrushSession.ts @@ -1,47 +1,65 @@ -import { Utils, TLBounds } from '@tldraw/core' -import { Vec } from '@tldraw/vec' -import { TLDrawSnapshot, Session, SessionType, TLDrawPatch, TLDrawStatus } from '~types' -import { TLDR } from '~state/TLDR' +import { TLBounds, Utils } from '@tldraw/core' +import { SessionType, TldrawPatch, TDStatus, TldrawCommand } from '~types' +import type { TldrawApp } from '../../internal' +import { BaseSession } from '../BaseSession' -export class BrushSession extends Session { - static type = SessionType.Brush - status = TLDrawStatus.Brushing - origin: number[] - snapshot: BrushSnapshot +export class BrushSession extends BaseSession { + type = SessionType.Brush + status = TDStatus.Brushing + initialSelectedIds: Set + shapesToTest: { + id: string + bounds: TLBounds + selectId: string + }[] - constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[]) { - super(viewport) - this.origin = Vec.round(point) - this.snapshot = getBrushSnapshot(data) + constructor(app: TldrawApp) { + super(app) + this.initialSelectedIds = new Set(this.app.selectedIds) + this.shapesToTest = this.app.shapes + .filter( + (shape) => + !( + shape.isLocked || + shape.isHidden || + shape.children !== undefined || + this.initialSelectedIds.has(shape.id) || + this.initialSelectedIds.has(shape.parentId) + ) + ) + .map((shape) => ({ + id: shape.id, + bounds: this.app.getShapeUtil(shape).getBounds(shape), + selectId: shape.id, //TLDR.getTopParentId(data, shape.id, currentPageId), + })) } - start = () => void null + start = (): TldrawPatch | undefined => void null - update = ( - data: TLDrawSnapshot, - point: number[], - _shiftKey = false, - _altKey = false, - metaKey = false - ): TLDrawPatch => { - const { snapshot, origin } = this - const { currentPageId } = data.appState + update = (): TldrawPatch | undefined => { + const { + initialSelectedIds, + shapesToTest, + app: { originPoint, currentPoint }, + } = this // Create a bounding box between the origin and the new point - const brush = Utils.getBoundsFromPoints([origin, point]) + const brush = Utils.getBoundsFromPoints([originPoint, currentPoint]) // Find ids of brushed shapes const hits = new Set() - const selectedIds = new Set(snapshot.selectedIds) - const page = TLDR.getPage(data, currentPageId) + const selectedIds = new Set(initialSelectedIds) - snapshot.shapesToTest.forEach(({ id, util, selectId }) => { + shapesToTest.forEach(({ id, selectId }) => { if (selectedIds.has(id)) return - const shape = page.shapes[id] + const { metaKey } = this.app + + const shape = this.app.getShape(id) if (!hits.has(selectId)) { + const util = this.app.getShapeUtil(shape) if ( metaKey ? Utils.boundsContain(brush, util.getBounds(shape)) @@ -59,7 +77,7 @@ export class BrushSession extends Session { } }) - const currentSelectedIds = data.document.pageStates[data.appState.currentPageId].selectedIds + const currentSelectedIds = this.app.selectedIds const didChange = selectedIds.size !== currentSelectedIds.length || @@ -70,7 +88,7 @@ export class BrushSession extends Session { return { document: { pageStates: { - [currentPageId]: { + [this.app.currentPageId]: { brush, selectedIds: afterSelectedIds, }, @@ -79,67 +97,29 @@ export class BrushSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { - const { currentPageId } = data.appState + cancel = (): TldrawPatch | undefined => { return { document: { pageStates: { - [currentPageId]: { + [this.app.currentPageId]: { brush: null, - selectedIds: this.snapshot.selectedIds, + selectedIds: Array.from(this.initialSelectedIds.values()), }, }, }, } } - complete = (data: TLDrawSnapshot) => { - const { currentPageId } = data.appState - const pageState = TLDR.getPageState(data, currentPageId) - + complete = (): TldrawPatch | TldrawCommand | undefined => { return { document: { pageStates: { - [currentPageId]: { + [this.app.currentPageId]: { brush: null, - selectedIds: [...pageState.selectedIds], + selectedIds: [...this.app.selectedIds], }, }, }, } } } - -/** - * Get a snapshot of the current selected ids, for each shape that is - * not already selected, the shape's id and a test to see whether the - * brush will intersect that shape. For tests, start broad -> fine. - */ -export function getBrushSnapshot(data: TLDrawSnapshot) { - const { currentPageId } = data.appState - const selectedIds = [...TLDR.getSelectedIds(data, currentPageId)] - - const shapesToTest = TLDR.getShapes(data, currentPageId) - .filter( - (shape) => - !( - shape.isHidden || - shape.children !== undefined || - selectedIds.includes(shape.id) || - selectedIds.includes(shape.parentId) - ) - ) - .map((shape) => ({ - id: shape.id, - util: TLDR.getShapeUtils(shape), - bounds: TLDR.getShapeUtils(shape).getBounds(shape), - selectId: shape.id, //TLDR.getTopParentId(data, shape.id, currentPageId), - })) - - return { - selectedIds, - shapesToTest, - } -} - -export type BrushSnapshot = ReturnType diff --git a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.spec.ts b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.spec.ts index d8a0eab16..8cfb41728 100644 --- a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.spec.ts +++ b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.spec.ts @@ -1,54 +1,42 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { - ColorStyle, - DashStyle, - SessionType, - SizeStyle, - TLDrawShapeType, - TLDrawStatus, -} from '~types' +import { TldrawTestApp } from '~test' +import { TDShapeType, TDStatus } from '~types' describe('Draw session', () => { - const state = new TLDrawState() - it('begins, updateSession', () => { - state.loadDocument(mockDocument) + const app = new TldrawTestApp() - expect(state.getShape('draw1')).toBe(undefined) - - state - .createShapes({ - id: 'draw1', - parentId: 'page1', - name: 'Draw', - childIndex: 5, - type: TLDrawShapeType.Draw, - point: [32, 32], - points: [[0, 0]], - style: { - dash: DashStyle.Draw, - size: SizeStyle.Medium, - color: ColorStyle.Blue, - }, - }) - .select('draw1') - .startSession(SessionType.Draw, [0, 0], 'draw1') - .updateSession([10, 10, 0.5]) + app + .selectTool(TDShapeType.Draw) + .pointCanvas([0, 0]) + .movePointer([10, 10, 0.5]) .completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + const shape = app.shapes[0] + + expect(shape).toBeTruthy() + + expect(app.status).toBe(TDStatus.Idle) }) it('does, undoes and redoes', () => { - expect(state.getShape('draw1')).toBeTruthy() + const app = new TldrawTestApp() - state.undo() + app + .selectTool(TDShapeType.Draw) + .pointCanvas([0, 0]) + .movePointer([10, 10, 0.5]) + .completeSession() - expect(state.getShape('draw1')).toBe(undefined) + const shape = app.shapes[0] - state.redo() + expect(app.getShape(shape.id)).toBeTruthy() - expect(state.getShape('draw1')).toBeTruthy() + app.undo() + + expect(app.getShape(shape.id)).toBe(undefined) + + app.redo() + + expect(app.getShape(shape.id)).toBeTruthy() }) }) diff --git a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts index 9890a62a5..fb800fbe4 100644 --- a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts +++ b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts @@ -1,46 +1,39 @@ -import { Utils, TLBounds } from '@tldraw/core' +import { Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { TLDrawSnapshot, Session, SessionType, TLDrawStatus } from '~types' -import { TLDR } from '~state/TLDR' +import { SessionType, TDStatus, TldrawPatch, TldrawCommand, DrawShape } from '~types' +import type { TldrawApp } from '../../internal' +import { BaseSession } from '../BaseSession' -export class DrawSession extends Session { - static type = SessionType.Draw - status = TLDrawStatus.Creating +export class DrawSession extends BaseSession { + type = SessionType.Draw + status = TDStatus.Creating topLeft: number[] - origin: number[] - previous: number[] - last: number[] points: number[][] + lastAdjustedPoint: number[] shiftedPoints: number[][] = [] shapeId: string isLocked?: boolean lockedDirection?: 'horizontal' | 'vertical' - constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[], id: string) { - super(viewport) - this.origin = point - this.previous = point - this.last = point - this.topLeft = point + constructor(app: TldrawApp, id: string) { + super(app) + const { originPoint } = this.app + this.topLeft = [...originPoint] this.shapeId = id // Add a first point but don't update the shape yet. We'll update // when the draw session ends; if the user hasn't added additional // points, this single point will be interpreted as a "dot" shape. - this.points = [[0, 0, point[2] || 0.5]] + this.points = [[0, 0, originPoint[2] || 0.5]] this.shiftedPoints = [...this.points] + this.lastAdjustedPoint = [0, 0] } - start = () => void null + start = (): TldrawPatch | undefined => void null - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { + update = (): TldrawPatch | undefined => { const { shapeId } = this + const { currentPoint, originPoint, shiftKey } = this.app // Even if we're not locked yet, we base the future locking direction // on the first dimension to reach a threshold, or the bigger dimension @@ -65,7 +58,7 @@ export class DrawSession extends Session { this.isLocked = true // Start locking - const returning = [...this.last] + const returning = [...this.lastAdjustedPoint] if (this.lockedDirection === 'vertical') { returning[0] = 0 @@ -73,8 +66,7 @@ export class DrawSession extends Session { returning[1] = 0 } - this.previous = returning - this.points.push(returning.concat(point[2])) + this.points.push(returning.concat(currentPoint[2])) } } else if (this.isLocked) { this.isLocked = false @@ -82,30 +74,33 @@ export class DrawSession extends Session { if (this.isLocked) { if (this.lockedDirection === 'vertical') { - point[0] = this.origin[0] + currentPoint[0] = originPoint[0] } else { - point[1] = this.origin[1] + currentPoint[1] = originPoint[1] } } // The new adjusted point - const newPoint = Vec.round(Vec.sub(point, this.origin)).concat(point[2]) + const newAdjustedPoint = Vec.round(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2]) // Don't add duplicate points. - if (Vec.isEqual(this.last, newPoint)) return + if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return // Add the new adjusted point to the points array - this.points.push(newPoint) + this.points.push(newAdjustedPoint) // The new adjusted point is now the previous adjusted point. - this.last = newPoint + this.lastAdjustedPoint = newAdjustedPoint // Does the input point create a new top left? const prevTopLeft = [...this.topLeft] - const topLeft = [Math.min(this.topLeft[0], point[0]), Math.min(this.topLeft[1], point[1])] + const topLeft = [ + Math.min(this.topLeft[0], currentPoint[0]), + Math.min(this.topLeft[1], currentPoint[1]), + ] - const delta = Vec.sub(topLeft, this.origin) + const delta = Vec.sub(topLeft, originPoint) // Time to shift some points! let points: number[][] @@ -123,7 +118,7 @@ export class DrawSession extends Session { // If the new top left is the same as the previous top left, // we don't need to shift anything: we just shift the new point // and add it to the shifted points array. - points = [...this.shiftedPoints, Vec.sub(newPoint, delta).concat(newPoint[2])] + points = [...this.shiftedPoints, Vec.sub(newAdjustedPoint, delta).concat(newAdjustedPoint[2])] } this.shiftedPoints = points @@ -131,7 +126,7 @@ export class DrawSession extends Session { return { document: { pages: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { shapes: { [shapeId]: { point: this.topLeft, @@ -141,7 +136,7 @@ export class DrawSession extends Session { }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { selectedIds: [shapeId], }, }, @@ -149,9 +144,9 @@ export class DrawSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { + cancel = (): TldrawPatch | undefined => { const { shapeId } = this - const pageId = data.appState.currentPageId + const pageId = this.app.currentPageId return { document: { @@ -171,9 +166,9 @@ export class DrawSession extends Session { } } - complete = (data: TLDrawSnapshot) => { + complete = (): TldrawPatch | TldrawCommand | undefined => { const { shapeId } = this - const pageId = data.appState.currentPageId + const pageId = this.app.currentPageId return { id: 'create_draw', @@ -199,14 +194,14 @@ export class DrawSession extends Session { [pageId]: { shapes: { [shapeId]: { - ...TLDR.getShape(data, shapeId, pageId), + ...this.app.getShape(shapeId), isComplete: true, }, }, }, }, pageStates: { - [data.appState.currentPageId]: { + [this.app.currentPageId]: { selectedIds: [], }, }, diff --git a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.spec.ts b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.spec.ts new file mode 100644 index 000000000..f3ac020a5 --- /dev/null +++ b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.spec.ts @@ -0,0 +1,41 @@ +import { mockDocument, TldrawTestApp } from '~test' +import { TDStatus } from '~types' + +describe('Draw session', () => { + it('begins, updates, completes session', () => { + const app = new TldrawTestApp().loadDocument(mockDocument) + + app.selectTool('erase').pointCanvas([300, 300]) + + expect(app.status).toBe('pointing') + + app.movePointer([0, 0]) + + expect(app.status).toBe('erasing') + + app.stopPointing() + + expect(app.appState.status).toBe(TDStatus.Idle) + + expect(app.shapes.length).toBe(0) + }) + + it('does, undoes and redoes', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .selectTool('erase') + .pointCanvas([300, 300]) + .movePointer([0, 0]) + .stopPointing() + + expect(app.shapes.length).toBe(0) + + app.undo() + + expect(app.shapes.length).toBe(3) + + app.redo() + + expect(app.shapes.length).toBe(0) + }) +}) diff --git a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts new file mode 100644 index 000000000..d6be9c09a --- /dev/null +++ b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts @@ -0,0 +1,203 @@ +import { Vec } from '@tldraw/vec' +import { + SessionType, + TDStatus, + TDShape, + PagePartial, + TDBinding, + TldrawPatch, + TldrawCommand, +} from '~types' +import type { TldrawApp } from '../../internal' +import { BaseSession } from '../BaseSession' + +export class EraseSession extends BaseSession { + type = SessionType.Draw + status = TDStatus.Creating + isLocked?: boolean + lockedDirection?: 'horizontal' | 'vertical' + erasedShapes = new Set() + erasedBindings = new Set() + initialSelectedShapes: TDShape[] + erasableShapes: TDShape[] + prevPoint: number[] + + constructor(app: TldrawApp) { + super(app) + this.prevPoint = [...app.originPoint] + this.initialSelectedShapes = this.app.selectedIds.map((id) => this.app.getShape(id)) + this.erasableShapes = this.app.shapes.filter((shape) => !shape.isLocked) + } + + start = (): TldrawPatch | undefined => void null + + update = (): TldrawPatch | undefined => { + const { page, shiftKey, originPoint, currentPoint } = this.app + + if (shiftKey) { + if (!this.isLocked && Vec.dist(originPoint, currentPoint) > 4) { + // If we're locking before knowing what direction we're in, set it + // early based on the bigger dimension. + if (!this.lockedDirection) { + const delta = Vec.sub(currentPoint, originPoint) + this.lockedDirection = delta[0] > delta[1] ? 'horizontal' : 'vertical' + } + + this.isLocked = true + } + } else if (this.isLocked) { + this.isLocked = false + } + + if (this.isLocked) { + if (this.lockedDirection === 'vertical') { + currentPoint[0] = originPoint[0] + } else { + currentPoint[1] = originPoint[1] + } + } + + const newPoint = Vec.round(Vec.add(originPoint, Vec.sub(currentPoint, originPoint))) + + const deletedShapeIds = new Set([]) + + for (const shape of this.erasableShapes) { + if (this.erasedShapes.has(shape)) continue + + if (this.app.getShapeUtil(shape).hitTestLineSegment(shape, this.prevPoint, newPoint)) { + this.erasedShapes.add(shape) + deletedShapeIds.add(shape.id) + + if (shape.children !== undefined) { + for (const childId of shape.children) { + this.erasedShapes.add(this.app.getShape(childId)) + deletedShapeIds.add(childId) + } + } + } + } + + // Erase bindings that reference deleted shapes + + Object.values(page.bindings).forEach((binding) => { + for (const id of [binding.toId, binding.fromId]) { + if (deletedShapeIds.has(id)) { + this.erasedBindings.add(binding) + } + } + }) + + const erasedShapes = Array.from(this.erasedShapes.values()) + + this.prevPoint = newPoint + + return { + document: { + pages: { + [page.id]: { + shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, { isGhost: true }])), + }, + }, + }, + } + } + + cancel = (): TldrawPatch | undefined => { + const { page } = this.app + + const erasedShapes = Array.from(this.erasedShapes.values()) + + return { + document: { + pages: { + [page.id]: { + shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, { isGhost: false }])), + }, + }, + pageStates: { + [page.id]: { + selectedIds: this.initialSelectedShapes.map((shape) => shape.id), + }, + }, + }, + } + } + + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { page } = this.app + + const erasedShapes = Array.from(this.erasedShapes.values()) + const erasedBindings = Array.from(this.erasedBindings.values()) + const erasedShapeIds = erasedShapes.map((shape) => shape.id) + const erasedBindingIds = erasedBindings.map((binding) => binding.id) + + const before: PagePartial = { + shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, shape])), + bindings: Object.fromEntries(erasedBindings.map((binding) => [binding.id, binding])), + } + + const after: PagePartial = { + shapes: Object.fromEntries(erasedShapes.map((shape) => [shape.id, undefined])), + bindings: Object.fromEntries(erasedBindings.map((binding) => [binding.id, undefined])), + } + + // Remove references on any shape's handles to any deleted bindings + this.app.shapes.forEach((shape) => { + if (shape.handles && !after.shapes[shape.id]) { + Object.values(shape.handles).forEach((handle) => { + if (handle.bindingId && erasedBindingIds.includes(handle.bindingId)) { + // Save the binding reference in the before patch + before.shapes[shape.id] = { + ...before.shapes[shape.id], + handles: { + ...before.shapes[shape.id]?.handles, + [handle.id]: handle, + }, + } + + // Save the binding reference in the before patch + if (!erasedShapeIds.includes(shape.id)) { + after.shapes[shape.id] = { + ...after.shapes[shape.id], + handles: { + ...after.shapes[shape.id]?.handles, + [handle.id]: undefined, + }, + } + } + } + }) + } + }) + + return { + id: 'erase', + before: { + document: { + pages: { + [page.id]: before, + }, + pageStates: { + [page.id]: { + selectedIds: this.initialSelectedShapes.map((shape) => shape.id), + }, + }, + }, + }, + after: { + document: { + pages: { + [page.id]: after, + }, + pageStates: { + [page.id]: { + selectedIds: this.initialSelectedShapes + .filter((shape) => !erasedShapeIds.includes(shape.id)) + .map((shape) => shape.id), + }, + }, + }, + }, + } + } +} diff --git a/packages/tldraw/src/state/sessions/EraseSession/index.ts b/packages/tldraw/src/state/sessions/EraseSession/index.ts new file mode 100644 index 000000000..85c9ea782 --- /dev/null +++ b/packages/tldraw/src/state/sessions/EraseSession/index.ts @@ -0,0 +1 @@ +export * from './EraseSession' diff --git a/packages/tldraw/src/state/sessions/GridSession/GridSession.spec.ts b/packages/tldraw/src/state/sessions/GridSession/GridSession.spec.ts index 38ae9a846..7450a27c8 100644 --- a/packages/tldraw/src/state/sessions/GridSession/GridSession.spec.ts +++ b/packages/tldraw/src/state/sessions/GridSession/GridSession.spec.ts @@ -1,42 +1,39 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { SessionType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { TDStatus } from '~types' describe('Grid session', () => { - const state = new TLDrawState() - it('begins, updateSession', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Translate, [5, 5]) - .updateSession([10, 10]) + .pointShape('rect1', [5, 5]) + .movePointer([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) - state.completeSession() + app.completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + expect(app.appState.status).toBe(TDStatus.Idle) - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) - state.undo() + app.undo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) - state.redo() + app.redo() - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) }) it('cancels session', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Translate, [5, 5]) - .updateSession([10, 10]) + .pointBounds([5, 5]) + .movePointer([10, 10]) .cancelSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) }) }) diff --git a/packages/tldraw/src/state/sessions/GridSession/GridSession.ts b/packages/tldraw/src/state/sessions/GridSession/GridSession.ts index 9f6d07561..02bb9f5d3 100644 --- a/packages/tldraw/src/state/sessions/GridSession/GridSession.ts +++ b/packages/tldraw/src/state/sessions/GridSession/GridSession.ts @@ -1,26 +1,15 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TLPageState, TLBounds, Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { - TLDrawShape, - TLDrawBinding, - Session, - TLDrawSnapshot, - TLDrawCommand, - TLDrawStatus, - ArrowShape, - GroupShape, - SessionType, - TLDrawShapeType, -} from '~types' -import { TLDR } from '~state/TLDR' +import { TDShape, TDStatus, SessionType, TDShapeType, TldrawPatch, TldrawCommand } from '~types' import type { Patch } from 'rko' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' -export class GridSession extends Session { +export class GridSession extends BaseSession { type = SessionType.Grid - status = TLDrawStatus.Translating - origin: number[] - shape: TLDrawShape + status = TDStatus.Translating + shape: TDShape bounds: TLBounds initialSelectedIds: string[] initialSiblings?: string[] @@ -29,58 +18,31 @@ export class GridSession extends Session { rows = 1 isCopying = false - constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - id: string, - pageId: string, - point: number[] - ) { - super(viewport) - this.origin = point - this.shape = TLDR.getShape(data, id, pageId) + constructor(app: TldrawApp, id: string) { + super(app) + this.shape = this.app.getShape(id) this.grid['0_0'] = this.shape.id - this.bounds = TLDR.getBounds(this.shape) - this.initialSelectedIds = TLDR.getSelectedIds(data, pageId) - if (this.shape.parentId !== pageId) { - this.initialSiblings = TLDR.getShape(data, this.shape.parentId, pageId).children?.filter( - (id) => id !== this.shape.id - ) + this.bounds = this.app.getShapeBounds(id) + this.initialSelectedIds = [...this.app.selectedIds] + if (this.shape.parentId !== this.app.currentPageId) { + this.initialSiblings = this.app + .getShape(this.shape.parentId) + .children?.filter((id) => id !== this.shape.id) } } - start = () => void null + start = (): TldrawPatch | undefined => void null - getClone = (point: number[], copy: boolean) => { - const clone = { - ...this.shape, - id: Utils.uniqueId(), - point, - } + update = (): TldrawPatch | undefined => { + const { currentPageId, altKey, shiftKey, currentPoint } = this.app - if (!copy) { - if (clone.type === TLDrawShapeType.Sticky) { - clone.text = '' - } - } - - return clone - } - - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { - const nextShapes: Patch> = {} + const nextShapes: Patch> = {} const nextPageState: Patch = {} const center = Utils.getBoundsCenter(this.bounds) - const offset = Vec.sub(point, center) + const offset = Vec.sub(currentPoint, center) if (shiftKey) { if (Math.abs(offset[0]) < Math.abs(offset[1])) { @@ -157,19 +119,20 @@ export class GridSession extends Session { return { document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: nextShapes, }, }, pageStates: { - [data.appState.currentPageId]: nextPageState, + [currentPageId]: nextPageState, }, }, } } - cancel = (data: TLDrawSnapshot) => { - const nextShapes: Record | undefined> = {} + cancel = (): TldrawPatch | undefined => { + const { currentPageId } = this.app + const nextShapes: Record | undefined> = {} // Delete clones Object.values(this.grid).forEach((id) => { @@ -189,12 +152,12 @@ export class GridSession extends Session { return { document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: nextShapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: [this.shape.id], }, }, @@ -202,18 +165,18 @@ export class GridSession extends Session { } } - complete = (data: TLDrawSnapshot) => { - const pageId = data.appState.currentPageId + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { currentPageId } = this.app - const beforeShapes: Patch> = {} + const beforeShapes: Patch> = {} - const afterShapes: Patch> = {} + const afterShapes: Patch> = {} const afterSelectedIds: string[] = [] Object.values(this.grid).forEach((id) => { beforeShapes[id] = undefined - afterShapes[id] = TLDR.getShape(data, id, pageId) + afterShapes[id] = this.app.getShape(id) afterSelectedIds.push(id) // TODO: Add shape to parent if grouped }) @@ -239,12 +202,12 @@ export class GridSession extends Session { before: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: beforeShapes, }, }, pageStates: { - [pageId]: { + [currentPageId]: { selectedIds: [], hoveredId: undefined, }, @@ -254,12 +217,12 @@ export class GridSession extends Session { after: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: afterShapes, }, }, pageStates: { - [pageId]: { + [currentPageId]: { selectedIds: afterSelectedIds, hoveredId: undefined, }, @@ -268,4 +231,20 @@ export class GridSession extends Session { }, } } + + private getClone = (point: number[], copy: boolean) => { + const clone = { + ...this.shape, + id: Utils.uniqueId(), + point, + } + + if (!copy) { + if (clone.type === TDShapeType.Sticky) { + clone.text = '' + } + } + + return clone + } } diff --git a/packages/tldraw/src/state/sessions/HandleSession/HandleSession.spec.ts b/packages/tldraw/src/state/sessions/HandleSession/HandleSession.spec.ts index c5388047b..695f8eda7 100644 --- a/packages/tldraw/src/state/sessions/HandleSession/HandleSession.spec.ts +++ b/packages/tldraw/src/state/sessions/HandleSession/HandleSession.spec.ts @@ -1,39 +1,38 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { SessionType, TLDrawShapeType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDShapeType, TDStatus } from '~types' describe('Handle session', () => { - const state = new TLDrawState() - it('begins, updateSession', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .createShapes({ id: 'arrow1', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, }) .select('arrow1') - .startSession(SessionType.Arrow, [-10, -10], 'end') - .updateSession([10, 10]) + .movePointer([-10, -10]) + .startSession(SessionType.Arrow, 'arrow1', 'end') + .movePointer([10, 10]) .completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + expect(app.status).toBe(TDStatus.Idle) - state.undo().redo() + app.undo().redo() }) it('cancels session', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .createShapes({ - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, id: 'arrow1', }) .select('arrow1') - .startSession(SessionType.Arrow, [-10, -10], 'end') - .updateSession([10, 10]) + .movePointer([-10, -10]) + .startSession(SessionType.Arrow, 'arrow1', 'end') + .movePointer([10, 10]) .cancelSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) }) }) diff --git a/packages/tldraw/src/state/sessions/HandleSession/HandleSession.ts b/packages/tldraw/src/state/sessions/HandleSession/HandleSession.ts index 47cb76b9c..9ffdbaf66 100644 --- a/packages/tldraw/src/state/sessions/HandleSession/HandleSession.ts +++ b/packages/tldraw/src/state/sessions/HandleSession/HandleSession.ts @@ -1,57 +1,44 @@ import { Vec } from '@tldraw/vec' -import type { TLBounds } from '@tldraw/core' -import { SessionType, ShapesWithProp, TLDrawStatus } from '~types' -import { Session } from '~types' -import type { TLDrawSnapshot } from '~types' +import { SessionType, ShapesWithProp, TldrawCommand, TldrawPatch, TDStatus } from '~types' import { TLDR } from '~state/TLDR' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' -export class HandleSession extends Session { - static type = SessionType.Handle - status = TLDrawStatus.TranslatingHandle +export class HandleSession extends BaseSession { + type = SessionType.Handle + status = TDStatus.TranslatingHandle commandId: string - delta = [0, 0] topLeft: number[] - origin: number[] shiftKey = false initialShape: ShapesWithProp<'handles'> handleId: string - constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - point: number[], - handleId: string, - commandId = 'move_handle' - ) { - super(viewport) - const { currentPageId } = data.appState - const shapeId = TLDR.getSelectedIds(data, currentPageId)[0] - this.topLeft = point - this.origin = point + constructor(app: TldrawApp, shapeId: string, handleId: string, commandId = 'move_handle') { + super(app) + const { originPoint } = app + this.topLeft = [...originPoint] this.handleId = handleId - this.initialShape = TLDR.getShape(data, shapeId, currentPageId) + this.initialShape = this.app.getShape(shapeId) this.commandId = commandId } - start = () => void null + start = (): TldrawPatch | undefined => void null - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { - const { initialShape } = this - const { currentPageId } = data.appState + update = (): TldrawPatch | undefined => { + const { + initialShape, + app: { currentPageId, currentPoint, shiftKey, altKey, metaKey }, + } = this - const shape = TLDR.getShape>(data, initialShape.id, currentPageId) + const shape = this.app.getShape>(initialShape.id) + + if (shape.isLocked) return void null const handles = shape.handles const handleId = this.handleId as keyof typeof handles - const delta = Vec.sub(point, handles[handleId].point) + const delta = Vec.sub(currentPoint, handles[handleId].point) const handle = { ...handles[handleId], @@ -60,7 +47,7 @@ export class HandleSession extends Session { // First update the handle's next point - const change = TLDR.getShapeUtils(shape).onHandleChange?.( + const change = TLDR.getShapeUtil(shape).onHandleChange?.( shape, { [handleId]: handle, @@ -68,7 +55,7 @@ export class HandleSession extends Session { { delta, shiftKey, altKey, metaKey } ) - if (!change) return data + if (!change) return return { document: { @@ -83,9 +70,11 @@ export class HandleSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { - const { initialShape } = this - const { currentPageId } = data.appState + cancel = (): TldrawPatch | undefined => { + const { + initialShape, + app: { currentPageId }, + } = this return { document: { @@ -100,16 +89,18 @@ export class HandleSession extends Session { } } - complete = (data: TLDrawSnapshot) => { - const { initialShape } = this - const pageId = data.appState.currentPageId + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { + initialShape, + app: { currentPageId }, + } = this return { id: this.commandId, before: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: { [initialShape.id]: initialShape, }, @@ -120,11 +111,9 @@ export class HandleSession extends Session { after: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: { - [initialShape.id]: TLDR.onSessionComplete( - TLDR.getShape(data, this.initialShape.id, pageId) - ), + [initialShape.id]: TLDR.onSessionComplete(this.app.getShape(this.initialShape.id)), }, }, }, diff --git a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts index 50bfb789e..63acc2fa4 100644 --- a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts +++ b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts @@ -1,135 +1,134 @@ import Vec from '@tldraw/vec' import { Utils } from '@tldraw/core' -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { SessionType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDStatus } from '~types' describe('Rotate session', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() - it('begins, updateSession', () => { - state.loadDocument(mockDocument) + it('begins, updates session', () => { + app.loadDocument(mockDocument) - expect(state.getShape('rect1').rotation).toBe(undefined) + expect(app.getShape('rect1').rotation).toBe(undefined) - state.select('rect1').startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]) + app.select('rect1').pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]) - expect(state.getShape('rect1').rotation).toBe(Math.PI / 2) + expect(app.getShape('rect1').rotation).toBe(Math.PI / 2) - state.updateSession([50, 100]) + app.movePointer([50, 100]) - expect(state.getShape('rect1').rotation).toBe(Math.PI) + expect(app.getShape('rect1').rotation).toBe(Math.PI) - state.updateSession([0, 50]) + app.movePointer([0, 50]) - expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) + expect(app.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) - state.updateSession([50, 0]) + app.movePointer([50, 0]) - expect(state.getShape('rect1').rotation).toBe(0) + expect(app.getShape('rect1').rotation).toBe(0) - state.updateSession([0, 50]) + app.movePointer([0, 50]) - expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) + expect(app.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) - state.completeSession() + app.completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + expect(app.appState.status).toBe(TDStatus.Idle) - state.undo() + app.undo() - expect(state.getShape('rect1').rotation).toBe(undefined) + expect(app.getShape('rect1').rotation).toBe(undefined) - state.redo() + app.redo() - expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) + expect(app.getShape('rect1').rotation).toBe((Math.PI * 3) / 2) }) it('cancels session', () => { - state + app .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Rotate, [50, 0]) - .updateSession([100, 50]) + .pointBoundsHandle('rotate', { x: 50, y: 0 }) + .movePointer([100, 50]) .cancel() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) }) it.todo('rotates handles only on shapes with handles') describe('when rotating a single shape while pressing shift', () => { it('Clamps rotation to 15 degrees', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() - state + app .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Rotate, [0, 0]) - .updateSession([20, 10], true) + .pointBoundsHandle('rotate', { x: 0, y: 0 }) + .movePointer({ x: 20, y: 10, shiftKey: true }) .completeSession() - expect(Math.round((state.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) + expect(Math.round((app.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) }) it('Clamps rotation to 15 degrees when starting from a rotation', () => { // Rect 1 is a little rotated - const state = new TLDrawState() + const app = new TldrawTestApp() - state + app .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Rotate, [0, 0]) - .updateSession([5, 5]) + .pointBoundsHandle('rotate', { x: 0, y: 0 }) + .movePointer([5, 5]) .completeSession() // Rect 1 clamp rotated, starting from a little rotation - state + app .select('rect1') - .startSession(SessionType.Rotate, [0, 0]) - .updateSession([100, 200], true) + .pointBoundsHandle('rotate', { x: 0, y: 0 }) + .movePointer({ x: 100, y: 200, shiftKey: true }) .completeSession() - expect(Math.round((state.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) + expect(Math.round((app.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) // Try again, too. - state + app .select('rect1') - .startSession(SessionType.Rotate, [0, 0]) - .updateSession([-100, 5000], true) + .pointBoundsHandle('rotate', { x: 0, y: 0 }) + .movePointer({ x: -100, y: 5000, shiftKey: true }) .completeSession() - expect(Math.round((state.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) + expect(Math.round((app.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0) }) }) describe('when rotating multiple shapes', () => { it('keeps the center', () => { - state.loadDocument(mockDocument).select('rect1', 'rect2') + app.loadDocument(mockDocument).select('rect1', 'rect2') const centerBefore = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) - state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession() + app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession() const centerAfterA = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) - state.startSession(SessionType.Rotate, [100, 0]).updateSession([50, 0]).completeSession() + app.pointBoundsHandle('rotate', { x: 100, y: 0 }).movePointer([50, 0]).completeSession() const centerAfterB = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) - expect(state.getShape('rect1').rotation) + expect(app.getShape('rect1').rotation) expect(centerBefore).toStrictEqual(centerAfterA) expect(centerAfterA).toStrictEqual(centerAfterB) }) @@ -141,32 +140,32 @@ describe('Rotate session', () => { it.todo('clears the cached center after any command other than a rotate command, tbh') it('changes the center after nudging', () => { - const state = new TLDrawState().loadDocument(mockDocument).select('rect1', 'rect2') + const app = new TldrawTestApp().loadDocument(mockDocument).select('rect1', 'rect2') const centerBefore = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) - state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession() + app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession() const centerAfterA = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) - expect(state.getShape('rect1').rotation) + expect(app.getShape('rect1').rotation) expect(centerBefore).toStrictEqual(centerAfterA) - state.selectAll().nudge([10, 10]) + app.selectAll().nudge([10, 10]) - state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession() + app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession() const centerAfterB = Vec.round( Utils.getBoundsCenter( - Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id))) + Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id))) ) ) diff --git a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.ts b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.ts index c8ae149d7..df87da94d 100644 --- a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.ts +++ b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.ts @@ -1,49 +1,82 @@ -import { Utils, TLBounds } from '@tldraw/core' +import { Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types' -import type { TLDrawSnapshot } from '~types' +import { SessionType, TldrawCommand, TldrawPatch, TDShape, TDStatus } from '~types' import { TLDR } from '~state/TLDR' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' -export class RotateSession extends Session { - static type = SessionType.Rotate - status = TLDrawStatus.Transforming +export class RotateSession extends BaseSession { + type = SessionType.Rotate + status = TDStatus.Transforming delta = [0, 0] - origin: number[] - snapshot: RotateSnapshot + commonBoundsCenter: number[] initialAngle: number - changes: Record> = {} + initialShapes: { + shape: TDShape + center: number[] + }[] + changes: Record> = {} - constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[]) { - super(viewport) + constructor(app: TldrawApp) { + super(app) - this.origin = point - this.snapshot = getRotateSnapshot(data) - this.initialAngle = Vec.angle(this.snapshot.commonBoundsCenter, this.origin) + const { + app: { currentPageId, pageState, originPoint }, + } = this + + const initialShapes = TLDR.getSelectedBranchSnapshot(app.state, currentPageId).filter( + (shape) => !shape.isLocked + ) + + if (initialShapes.length === 0) { + throw Error('No selected shapes!') + } + + if (app.rotationInfo.selectedIds === pageState.selectedIds) { + if (app.rotationInfo.center === undefined) { + throw Error('We should have a center for rotation!') + } + + this.commonBoundsCenter = app.rotationInfo.center + } else { + this.commonBoundsCenter = Utils.getBoundsCenter( + Utils.getCommonBounds(initialShapes.map(TLDR.getBounds)) + ) + app.rotationInfo.selectedIds = pageState.selectedIds + app.rotationInfo.center = this.commonBoundsCenter + } + + this.initialShapes = initialShapes + .filter((shape) => shape.children === undefined) + .map((shape) => { + return { + shape, + center: this.app.getShapeUtil(shape).getCenter(shape), + } + }) + + this.initialAngle = Vec.angle(this.commonBoundsCenter, originPoint) } - start = () => void null + start = (): TldrawPatch | undefined => void null - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { - const { commonBoundsCenter, initialShapes } = this.snapshot + update = (): TldrawPatch | undefined => { + const { + commonBoundsCenter, + initialShapes, + app: { currentPageId, currentPoint, shiftKey }, + } = this - const pageId = data.appState.currentPageId + const shapes: Record> = {} - const shapes: Record> = {} - - let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle + let directionDelta = Vec.angle(commonBoundsCenter, currentPoint) - this.initialAngle if (shiftKey) { directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees } // Update the shapes - initialShapes.forEach(({ id, center, shape }) => { + initialShapes.forEach(({ center, shape }) => { const { rotation = 0 } = shape let shapeDelta = 0 @@ -60,7 +93,7 @@ export class RotateSession extends Session { ) if (change) { - shapes[id] = change + shapes[shape.id] = change } }) @@ -69,7 +102,7 @@ export class RotateSession extends Session { return { document: { pages: { - [pageId]: { + [currentPageId]: { shapes, }, }, @@ -77,20 +110,19 @@ export class RotateSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { - const { initialShapes } = this.snapshot - const pageId = data.appState.currentPageId + cancel = (): TldrawPatch | undefined => { + const { + initialShapes, + app: { currentPageId }, + } = this - const shapes: Record = {} - - for (const { id, shape } of initialShapes) { - shapes[id] = shape - } + const shapes: Record = {} + initialShapes.forEach(({ shape }) => (shapes[shape.id] = shape)) return { document: { pages: { - [pageId]: { + [currentPageId]: { shapes, }, }, @@ -98,14 +130,16 @@ export class RotateSession extends Session { } } - complete = (data: TLDrawSnapshot) => { - const { initialShapes } = this.snapshot - const pageId = data.appState.currentPageId + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { + initialShapes, + app: { currentPageId }, + } = this - const beforeShapes = {} as Record> + const beforeShapes = {} as Record> const afterShapes = this.changes - initialShapes.forEach(({ id, shape: { point, rotation, handles } }) => { + initialShapes.forEach(({ shape: { id, point, rotation, handles } }) => { beforeShapes[id] = { point, rotation, handles } }) @@ -114,7 +148,7 @@ export class RotateSession extends Session { before: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: beforeShapes, }, }, @@ -123,7 +157,7 @@ export class RotateSession extends Session { after: { document: { pages: { - [pageId]: { + [currentPageId]: { shapes: afterShapes, }, }, @@ -132,46 +166,3 @@ export class RotateSession extends Session { } } } - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function getRotateSnapshot(data: TLDrawSnapshot) { - const currentPageId = data.appState.currentPageId - const pageState = TLDR.getPageState(data, currentPageId) - const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId) - - if (initialShapes.length === 0) { - throw Error('No selected shapes!') - } - - let commonBoundsCenter: number[] - - if (Session.cache.selectedIds === pageState.selectedIds) { - if (Session.cache.center === undefined) { - throw Error('The center was not added to the cache!') - } - - commonBoundsCenter = Session.cache.center - } else { - commonBoundsCenter = Utils.getBoundsCenter( - Utils.getCommonBounds(initialShapes.map(TLDR.getBounds)) - ) - Session.cache.selectedIds = pageState.selectedIds - Session.cache.center = commonBoundsCenter - } - - return { - commonBoundsCenter, - initialShapes: initialShapes - .filter((shape) => shape.children === undefined) - .map((shape) => { - const center = TLDR.getShapeUtils(shape).getCenter(shape) - return { - id: shape.id, - shape, - center, - } - }), - } -} - -export type RotateSnapshot = ReturnType diff --git a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.spec.ts b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.spec.ts index 926d9f47f..510f057a8 100644 --- a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.spec.ts +++ b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.spec.ts @@ -1,22 +1,19 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' +import { mockDocument, TldrawTestApp } from '~test' import { TLBoundsCorner, Utils } from '@tldraw/core' import { TLDR } from '~state/TLDR' -import { SessionType, TLDrawStatus } from '~types' +import { TDShapeType, TDStatus } from '~types' -function getShapeBounds(state: TLDrawState, ...ids: string[]) { +function getShapeBounds(app: TldrawTestApp, ...ids: string[]) { return Utils.getCommonBounds( - (ids.length ? ids : state.selectedIds).map((id) => TLDR.getBounds(state.getShape(id))) + (ids.length ? ids : app.selectedIds).map((id) => TLDR.getBounds(app.getShape(id))) ) } describe('Transform session', () => { - const state = new TLDrawState() - it('begins, updateSession', () => { - state.loadDocument(mockDocument) + const app = new TldrawTestApp().loadDocument(mockDocument) - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 0, minY: 0, maxX: 100, @@ -25,15 +22,15 @@ describe('Transform session', () => { height: 100, }) - state + app .select('rect1', 'rect2') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer([10, 10]) .completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + expect(app.status).toBe(TDStatus.Idle) - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 10, minY: 10, maxX: 105, @@ -42,9 +39,9 @@ describe('Transform session', () => { height: 95, }) - state.undo() + app.undo() - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 0, minY: 0, maxX: 100, @@ -53,30 +50,30 @@ describe('Transform session', () => { height: 100, }) - state.redo() + app.redo() }) it('cancels session', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 5, y: 5 }) + .movePointer([10, 10]) .cancelSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) }) describe('when transforming from the top-left corner', () => { it('transforms a single shape', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer([10, 10]) .completeSession() - expect(getShapeBounds(state)).toMatchObject({ + expect(getShapeBounds(app)).toMatchObject({ minX: 10, minY: 10, maxX: 100, @@ -87,14 +84,14 @@ describe('Transform session', () => { }) it('transforms a single shape while holding shift', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([20, 10], true) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer({ x: 20, y: 10, shiftKey: true }) .completeSession() - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 10, minY: 10, maxX: 100, @@ -105,14 +102,14 @@ describe('Transform session', () => { }) it('transforms multiple shapes', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer([10, 10]) .completeSession() - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 10, minY: 10, maxX: 105, @@ -121,7 +118,7 @@ describe('Transform session', () => { height: 95, }) - expect(getShapeBounds(state, 'rect2')).toMatchObject({ + expect(getShapeBounds(app, 'rect2')).toMatchObject({ minX: 105, minY: 105, maxX: 200, @@ -132,14 +129,14 @@ describe('Transform session', () => { }) it('transforms multiple shapes while holding shift', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([20, 10], true) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer({ x: 20, y: 10, shiftKey: true }) .completeSession() - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 10, minY: 10, maxX: 105, @@ -148,7 +145,7 @@ describe('Transform session', () => { height: 95, }) - expect(getShapeBounds(state, 'rect2')).toMatchObject({ + expect(getShapeBounds(app, 'rect2')).toMatchObject({ minX: 105, minY: 105, maxX: 200, @@ -189,16 +186,15 @@ describe('Transform session', () => { describe('when transforming a group', () => { it('transforms the groups children', () => { - const state = new TLDrawState() - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .group(['rect1', 'rect2'], 'groupA') .select('groupA') - .startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 0, y: 0 }) + .movePointer([10, 10]) .completeSession() - expect(getShapeBounds(state, 'rect1')).toMatchObject({ + expect(getShapeBounds(app, 'rect1')).toMatchObject({ minX: 10, minY: 10, maxX: 105, @@ -207,7 +203,7 @@ describe('Transform session', () => { height: 95, }) - expect(getShapeBounds(state, 'rect2')).toMatchObject({ + expect(getShapeBounds(app, 'rect2')).toMatchObject({ minX: 105, minY: 105, maxX: 200, @@ -221,15 +217,17 @@ describe('Transform session', () => { describe('When creating with a transform session', () => { it('Deletes the shape on undo', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft, true) - .updateSession([10, 10]) - .completeSession() - .undo() + const app = new TldrawTestApp() + .selectTool(TDShapeType.Rectangle) + .pointCanvas([0, 0]) + .movePointer([10, 10]) + .stopPointing() - expect(state.getShape('rect1')).toBe(undefined) + expect(app.shapes.length).toBe(1) + + app.undo() + + expect(app.shapes.length).toBe(0) }) }) diff --git a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts index 77d4beb1a..cb7484db8 100644 --- a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts +++ b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts @@ -1,11 +1,11 @@ import { TLBounds, TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' import type { TLSnapLine, TLBoundsWithCenter } from '@tldraw/core' -import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types' -import type { TLDrawSnapshot } from '~types' +import { SessionType, TldrawCommand, TldrawPatch, TDShape, TDStatus } from '~types' import { TLDR } from '~state/TLDR' -import type { Command } from 'rko' import { SLOW_SPEED, SNAP_DISTANCE } from '~constants' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' type SnapInfo = | { @@ -16,60 +16,112 @@ type SnapInfo = bounds: TLBoundsWithCenter[] } -export class TransformSession extends Session { - static type = SessionType.Transform - status = TLDrawStatus.Transforming +export class TransformSession extends BaseSession { + type = SessionType.Transform + status = TDStatus.Transforming scaleX = 1 scaleY = 1 - transformType: TLBoundsEdge | TLBoundsCorner - origin: number[] - snapshot: TransformSnapshot - isCreate: boolean + initialShapes: TDShape[] + initialShapeIds: string[] initialSelectedIds: string[] + shapeBounds: { + initialShape: TDShape + initialShapeBounds: TLBounds + transformOrigin: number[] + }[] + hasUnlockedShapes: boolean + isAllAspectRatioLocked: boolean + initialCommonBounds: TLBounds snapInfo: SnapInfo = { state: 'empty' } prevPoint = [0, 0] speed = 1 constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - point: number[], - transformType: TLBoundsEdge | TLBoundsCorner = TLBoundsCorner.BottomRight, - isCreate = false + app: TldrawApp, + public transformType: TLBoundsEdge | TLBoundsCorner = TLBoundsCorner.BottomRight, + public isCreate = false ) { - super(viewport) - this.origin = point - this.transformType = transformType - this.snapshot = getTransformSnapshot(data, transformType) - this.isCreate = isCreate - this.initialSelectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId) - Session.cache.selectedIds = [...this.initialSelectedIds] + super(app) + this.initialSelectedIds = [...this.app.selectedIds] + this.app.rotationInfo.selectedIds = [...this.initialSelectedIds] + + this.initialShapes = TLDR.getSelectedBranchSnapshot( + this.app.state, + this.app.currentPageId + ).filter((shape) => !shape.isLocked) + + this.initialShapeIds = this.initialShapes.map((shape) => shape.id) + + this.hasUnlockedShapes = this.initialShapes.length > 0 + + this.isAllAspectRatioLocked = this.initialShapes.every( + (shape) => shape.isAspectRatioLocked || TLDR.getShapeUtil(shape).isAspectRatioLocked + ) + + const shapesBounds = Object.fromEntries( + this.initialShapes.map((shape) => [shape.id, TLDR.getBounds(shape)]) + ) + + const boundsArr = Object.values(shapesBounds) + + this.initialCommonBounds = Utils.getCommonBounds(boundsArr) + + const initialInnerBounds = Utils.getBoundsFromPoints(boundsArr.map(Utils.getBoundsCenter)) + + // Return a mapping of shapes to bounds together with the relative + // positions of the shape's bounds within the common bounds shape. + this.shapeBounds = this.initialShapes.map((shape) => { + const initialShapeBounds = shapesBounds[shape.id] + const ic = Utils.getBoundsCenter(initialShapeBounds) + + const ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width + const iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height + + return { + initialShape: shape, + initialShapeBounds, + transformOrigin: [ix, iy], + } + }) } - start = (data: TLDrawSnapshot) => { - this.createSnapInfo(data) + start = (): TldrawPatch | undefined => { + this.snapInfo = { + state: 'ready', + bounds: this.app.shapes + .filter((shape) => !this.initialShapeIds.includes(shape.id)) + .map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))), + } + return void null } // eslint-disable-next-line @typescript-eslint/no-unused-vars - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { + update = (): TldrawPatch | undefined => { const { transformType, - snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked }, + shapeBounds, + initialCommonBounds, + isAllAspectRatioLocked, + app: { + currentPageId, + pageState: { camera }, + viewport, + currentPoint, + previousPoint, + originPoint, + shiftKey, + metaKey, + settings: { isSnapping }, + }, } = this - const shapes = {} as Record + const shapes = {} as Record - const delta = Vec.sub(point, this.origin) + const delta = Vec.sub(currentPoint, originPoint) let newBounds = Utils.getTransformedBoundingBox( - initialBounds, + initialCommonBounds, transformType, delta, 0, @@ -78,9 +130,7 @@ export class TransformSession extends Session { // Should we snap? - const speed = Vec.dist(point, this.prevPoint) - - this.prevPoint = point + const speed = Vec.dist(currentPoint, previousPoint) const speedChange = speed - this.speed @@ -88,29 +138,24 @@ export class TransformSession extends Session { let snapLines: TLSnapLine[] = [] - const { currentPageId } = data.appState - - const { zoom } = data.document.pageStates[currentPageId].camera - if ( - ((data.settings.isSnapping && !metaKey) || (!data.settings.isSnapping && metaKey)) && - this.speed * zoom < SLOW_SPEED && + ((isSnapping && !metaKey) || (!isSnapping && metaKey)) && + this.speed * camera.zoom < SLOW_SPEED && this.snapInfo.state === 'ready' ) { const snapResult = Utils.getSnapPoints( Utils.getBoundsWithCenter(newBounds), this.snapInfo.bounds.filter( - (bounds) => - Utils.boundsContain(this.viewport, bounds) || Utils.boundsCollide(this.viewport, bounds) + (bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds) ), - SNAP_DISTANCE / zoom + SNAP_DISTANCE / camera.zoom ) if (snapResult) { snapLines = snapResult.snapLines newBounds = Utils.getTransformedBoundingBox( - initialBounds, + initialCommonBounds, transformType, Vec.sub(delta, snapResult.offset), 0, @@ -124,26 +169,22 @@ export class TransformSession extends Session { this.scaleX = newBounds.scaleX this.scaleY = newBounds.scaleY - shapeBounds.forEach(({ id, initialShape, initialShapeBounds, transformOrigin }) => { + shapeBounds.forEach(({ initialShape, initialShapeBounds, transformOrigin }) => { const newShapeBounds = Utils.getRelativeTransformedBoundingBox( newBounds, - initialBounds, + initialCommonBounds, initialShapeBounds, this.scaleX < 0, this.scaleY < 0 ) - shapes[id] = TLDR.transform( - TLDR.getShape(data, id, data.appState.currentPageId), - newShapeBounds, - { - type: this.transformType, - initialShape, - scaleX: this.scaleX, - scaleY: this.scaleY, - transformOrigin, - } - ) + shapes[initialShape.id] = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, { + type: this.transformType, + initialShape, + scaleX: this.scaleX, + scaleY: this.scaleY, + transformOrigin, + }) }) return { @@ -152,7 +193,7 @@ export class TransformSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes, }, }, @@ -160,15 +201,18 @@ export class TransformSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { - const { shapeBounds } = this.snapshot + cancel = (): TldrawPatch | undefined => { + const { + shapeBounds, + app: { currentPageId }, + } = this - const shapes = {} as Record + const shapes = {} as Record if (this.isCreate) { - shapeBounds.forEach((shape) => (shapes[shape.id] = undefined)) + shapeBounds.forEach((shape) => (shapes[shape.initialShape.id] = undefined)) } else { - shapeBounds.forEach((shape) => (shapes[shape.id] = shape.initialShape)) + shapeBounds.forEach((shape) => (shapes[shape.initialShape.id] = shape.initialShape)) } return { @@ -177,43 +221,48 @@ export class TransformSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes, }, }, pageStates: { - [data.appState.currentPageId]: { - selectedIds: this.isCreate ? [] : shapeBounds.map((shape) => shape.id), + [currentPageId]: { + selectedIds: this.isCreate ? [] : shapeBounds.map((shape) => shape.initialShape.id), }, }, }, } } - complete = (data: TLDrawSnapshot): TLDrawSnapshot | Command | undefined => { - const { hasUnlockedShapes, shapeBounds } = this.snapshot - undefined + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { + isCreate, + shapeBounds, + hasUnlockedShapes, + app: { currentPageId }, + } = this + if (!hasUnlockedShapes) return - const beforeShapes: Record = {} - const afterShapes: Record = {} + const beforeShapes: Record = {} + const afterShapes: Record = {} let beforeSelectedIds: string[] let afterSelectedIds: string[] - if (this.isCreate) { + if (isCreate) { beforeSelectedIds = [] afterSelectedIds = [] - shapeBounds.forEach((shape) => { - beforeShapes[shape.id] = undefined - afterShapes[shape.id] = TLDR.getShape(data, shape.id, data.appState.currentPageId) + shapeBounds.forEach(({ initialShape }) => { + beforeShapes[initialShape.id] = undefined + afterShapes[initialShape.id] = this.app.getShape(initialShape.id) }) } else { beforeSelectedIds = this.initialSelectedIds afterSelectedIds = this.initialSelectedIds - shapeBounds.forEach((shape) => { - beforeShapes[shape.id] = shape.initialShape - afterShapes[shape.id] = TLDR.getShape(data, shape.id, data.appState.currentPageId) + shapeBounds.forEach(({ initialShape }) => { + beforeShapes[initialShape.id] = initialShape + afterShapes[initialShape.id] = this.app.getShape(initialShape.id) }) } @@ -225,12 +274,12 @@ export class TransformSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: beforeShapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: beforeSelectedIds, hoveredId: undefined, editingId: undefined, @@ -244,12 +293,12 @@ export class TransformSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: afterShapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: afterSelectedIds, hoveredId: undefined, editingId: undefined, @@ -259,69 +308,4 @@ export class TransformSession extends Session { }, } } - - private createSnapInfo = async (data: TLDrawSnapshot) => { - const { initialShapeIds } = this.snapshot - const { currentPageId } = data.appState - const page = data.document.pages[currentPageId] - - this.snapInfo = { - state: 'ready', - bounds: Object.values(page.shapes) - .filter((shape) => !initialShapeIds.includes(shape.id)) - .map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))), - } - } } - -export function getTransformSnapshot( - data: TLDrawSnapshot, - transformType: TLBoundsEdge | TLBoundsCorner -) { - const initialShapes = TLDR.getSelectedBranchSnapshot(data, data.appState.currentPageId) - - const initialShapeIds = initialShapes.map((shape) => shape.id) - - const hasUnlockedShapes = initialShapes.length > 0 - - const isAllAspectRatioLocked = initialShapes.every( - (shape) => shape.isAspectRatioLocked || TLDR.getShapeUtils(shape).isAspectRatioLocked - ) - - const shapesBounds = Object.fromEntries( - initialShapes.map((shape) => [shape.id, TLDR.getBounds(shape)]) - ) - - const boundsArr = Object.values(shapesBounds) - - const commonBounds = Utils.getCommonBounds(boundsArr) - - const initialInnerBounds = Utils.getBoundsFromPoints(boundsArr.map(Utils.getBoundsCenter)) - - // Return a mapping of shapes to bounds together with the relative - // positions of the shape's bounds within the common bounds shape. - return { - type: transformType, - hasUnlockedShapes, - isAllAspectRatioLocked, - initialShapeIds, - initialShapes, - initialBounds: commonBounds, - shapeBounds: initialShapes.map((shape) => { - const initialShapeBounds = shapesBounds[shape.id] - const ic = Utils.getBoundsCenter(initialShapeBounds) - - const ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width - const iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height - - return { - id: shape.id, - initialShape: shape, - initialShapeBounds, - transformOrigin: [ix, iy], - } - }), - } -} - -export type TransformSnapshot = ReturnType diff --git a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.spec.ts b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.spec.ts deleted file mode 100644 index 8c5f8a216..000000000 --- a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { TLBoundsCorner } from '@tldraw/core' -import { SessionType, TLDrawStatus } from '~types' - -describe('Transform single session', () => { - const state = new TLDrawState() - - it('begins, updateSession', () => { - state - .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.TransformSingle, [-10, -10], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) - .completeSession() - - expect(state.appState.status).toBe(TLDrawStatus.Idle) - - state.undo().redo() - }) - - it('cancels session', () => { - state - .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.TransformSingle, [5, 5], TLBoundsCorner.TopLeft) - .updateSession([10, 10]) - .cancelSession() - - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - }) -}) - -describe('When creating with a transform-single session', () => { - it('Deletes the shape on undo', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.TransformSingle, [5, 5], TLBoundsCorner.TopLeft, true) - .updateSession([10, 10]) - .completeSession() - .undo() - - expect(state.getShape('rect1')).toBe(undefined) - }) -}) - -describe('When snapping', () => { - it.todo('Does not snap when moving quicky') - it.todo('Snaps only matching edges when moving slowly') - it.todo('Snaps any edge to any edge when moving very slowly') - it.todo('Snaps a clone to its parent') - it.todo('Cleans up snap lines when cancelled') - it.todo('Cleans up snap lines when completed') - it.todo('Cleans up snap lines when starting to clone / not clone') - it.todo('Snaps the rotated bounding box of rotated shapes') - it.todo('Snaps to a shape on screen') - it.todo('Does not snap to a shape off screen.') - it.todo('Snaps while panning.') -}) diff --git a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.spec.ts b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.spec.ts new file mode 100644 index 000000000..bdfb98192 --- /dev/null +++ b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.spec.ts @@ -0,0 +1,41 @@ +import { mockDocument, TldrawTestApp } from '~test' +import { TLBoundsCorner } from '@tldraw/core' +import { TDStatus } from '~types' + +describe('Transform single session', () => { + it('begins, updateSession', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .select('rect1') + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: -10, y: -10 }) + .stopPointing() + + expect(app.status).toBe(TDStatus.Idle) + + app.undo().redo() + }) + + it('cancels session', () => { + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .select('rect1') + .pointBoundsHandle(TLBoundsCorner.TopLeft, { x: 5, y: 5 }) + .cancelSession() + + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + }) +}) + +describe('When snapping', () => { + it.todo('Does not snap when moving quicky') + it.todo('Snaps only matching edges when moving slowly') + it.todo('Snaps any edge to any edge when moving very slowly') + it.todo('Snaps a clone to its parent') + it.todo('Cleans up snap lines when cancelled') + it.todo('Cleans up snap lines when completed') + it.todo('Cleans up snap lines when starting to clone / not clone') + it.todo('Snaps the rotated bounding box of rotated shapes') + it.todo('Snaps to a shape on screen') + it.todo('Does not snap to a shape off screen.') + it.todo('Snaps while panning.') +}) diff --git a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.ts b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts similarity index 50% rename from packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.ts rename to packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts index a500a5f08..ad1db2b80 100644 --- a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.session.ts +++ b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts @@ -1,17 +1,17 @@ import { - TLBounds, TLBoundsCorner, TLSnapLine, TLBoundsEdge, Utils, TLBoundsWithCenter, + TLBounds, } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { SessionType, TLDrawShape, TLDrawStatus } from '~types' -import { Session } from '~types' -import type { TLDrawSnapshot } from '~types' +import { SessionType, TldrawCommand, TldrawPatch, TDShape, TDStatus } from '~types' import { TLDR } from '~state/TLDR' import { SLOW_SPEED, SNAP_DISTANCE } from '~constants' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' type SnapInfo = | { @@ -22,57 +22,75 @@ type SnapInfo = bounds: TLBoundsWithCenter[] } -export class TransformSingleSession extends Session { +export class TransformSingleSession extends BaseSession { type = SessionType.TransformSingle - status = TLDrawStatus.Transforming + status = TDStatus.Transforming transformType: TLBoundsEdge | TLBoundsCorner - origin: number[] scaleX = 1 scaleY = 1 isCreate: boolean - snapshot: TransformSingleSnapshot + initialShape: TDShape + initialShapeBounds: TLBounds + initialCommonBounds: TLBounds snapInfo: SnapInfo = { state: 'empty' } prevPoint = [0, 0] speed = 1 constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - point: number[], - transformType: TLBoundsEdge | TLBoundsCorner = TLBoundsCorner.BottomRight, + app: TldrawApp, + id: string, + transformType: TLBoundsEdge | TLBoundsCorner, isCreate = false ) { - super(viewport) - this.origin = point - this.transformType = transformType - this.snapshot = getTransformSingleSnapshot(data, transformType) + super(app) this.isCreate = isCreate - Session.cache.selectedIds = [...this.snapshot.initialShape.id] + this.transformType = transformType + + const shape = this.app.getShape(id) + this.initialShape = shape + this.initialShapeBounds = TLDR.getBounds(shape) + this.initialCommonBounds = TLDR.getRotatedBounds(shape) + this.app.rotationInfo.selectedIds = [shape.id] } - start = (data: TLDrawSnapshot) => { - this.createSnapInfo(data) + start = (): TldrawPatch | undefined => { + this.snapInfo = { + state: 'ready', + bounds: this.app.shapes + .filter((shape) => shape.id !== this.initialShape.id) + .map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))), + } + return void null } - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - _altKey = false, - metaKey = false - ) => { - const { transformType } = this + update = (): TldrawPatch | undefined => { + const { + transformType, + initialShape, + initialShapeBounds, + app: { + settings: { isSnapping }, + currentPageId, + pageState: { camera }, + viewport, + currentPoint, + previousPoint, + originPoint, + shiftKey, + metaKey, + }, + } = this - const { currentPageId, initialShapeBounds, initialShape, id } = this.snapshot + if (initialShape.isLocked) return void null - const delta = Vec.sub(point, this.origin) + const delta = Vec.sub(currentPoint, originPoint) - const shapes = {} as Record> + const shapes = {} as Record> - const shape = TLDR.getShape(data, id, data.appState.currentPageId) + const shape = this.app.getShape(initialShape.id) - const utils = TLDR.getShapeUtils(shape) + const utils = TLDR.getShapeUtil(shape) let newBounds = Utils.getTransformedBoundingBox( initialShapeBounds, @@ -84,9 +102,7 @@ export class TransformSingleSession extends Session { // Should we snap? - const speed = Vec.dist(point, this.prevPoint) - - this.prevPoint = point + const speed = Vec.dist(currentPoint, previousPoint) const speedChange = speed - this.speed @@ -94,21 +110,18 @@ export class TransformSingleSession extends Session { let snapLines: TLSnapLine[] = [] - const { zoom } = data.document.pageStates[currentPageId].camera - if ( - ((data.settings.isSnapping && !metaKey) || (!data.settings.isSnapping && metaKey)) && + ((isSnapping && !metaKey) || (!isSnapping && metaKey)) && !initialShape.rotation && // not now anyway - this.speed * zoom < SLOW_SPEED && + this.speed * camera.zoom < SLOW_SPEED && this.snapInfo.state === 'ready' ) { const snapResult = Utils.getSnapPoints( Utils.getBoundsWithCenter(newBounds), this.snapInfo.bounds.filter( - (bounds) => - Utils.boundsContain(this.viewport, bounds) || Utils.boundsCollide(this.viewport, bounds) + (bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds) ), - SNAP_DISTANCE / zoom + SNAP_DISTANCE / camera.zoom ) if (snapResult) { @@ -124,7 +137,7 @@ export class TransformSingleSession extends Session { } } - const afterShape = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { + const afterShape = TLDR.getShapeUtil(shape).transformSingle(shape, newBounds, { initialShape, type: this.transformType, scaleX: newBounds.scaleX, @@ -142,7 +155,7 @@ export class TransformSingleSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes, }, }, @@ -150,10 +163,13 @@ export class TransformSingleSession extends Session { } } - cancel = (data: TLDrawSnapshot) => { - const { initialShape } = this.snapshot + cancel = (): TldrawPatch | undefined => { + const { + initialShape, + app: { currentPageId }, + } = this - const shapes = {} as Record + const shapes = {} as Record if (this.isCreate) { shapes[initialShape.id] = undefined @@ -167,12 +183,12 @@ export class TransformSingleSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: this.isCreate ? [] : [initialShape.id], }, }, @@ -180,19 +196,20 @@ export class TransformSingleSession extends Session { } } - complete = (data: TLDrawSnapshot) => { - if (!this.snapshot.hasUnlockedShape) return data + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { + initialShape, + app: { currentPageId }, + } = this - const { initialShape } = this.snapshot + if (initialShape.isLocked) return - const beforeShapes = {} as Record | undefined> - const afterShapes = {} as Record> + const beforeShapes = {} as Record | undefined> + const afterShapes = {} as Record> beforeShapes[initialShape.id] = this.isCreate ? undefined : initialShape - afterShapes[initialShape.id] = TLDR.onSessionComplete( - TLDR.getShape(data, initialShape.id, data.appState.currentPageId) - ) + afterShapes[initialShape.id] = TLDR.onSessionComplete(this.app.getShape(initialShape.id)) return { id: 'transform_single', @@ -202,12 +219,12 @@ export class TransformSingleSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: beforeShapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: this.isCreate ? [] : [initialShape.id], editingId: undefined, hoveredId: undefined, @@ -221,12 +238,12 @@ export class TransformSingleSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: afterShapes, }, }, pageStates: { - [data.appState.currentPageId]: { + [currentPageId]: { selectedIds: [initialShape.id], editingId: undefined, hoveredId: undefined, @@ -236,41 +253,4 @@ export class TransformSingleSession extends Session { }, } } - - private createSnapInfo = async (data: TLDrawSnapshot) => { - const { initialShape } = this.snapshot - const { currentPageId } = data.appState - const page = data.document.pages[currentPageId] - - this.snapInfo = { - state: 'ready', - bounds: Object.values(page.shapes) - .filter((shape) => shape.id !== initialShape.id) - .map((shape) => Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape))), - } - } } - -export function getTransformSingleSnapshot( - data: TLDrawSnapshot, - transformType: TLBoundsEdge | TLBoundsCorner -) { - const { currentPageId } = data.appState - const shape = TLDR.getShape(data, TLDR.getSelectedIds(data, currentPageId)[0], currentPageId) - - if (!shape) { - throw Error('You must have one shape selected.') - } - - return { - id: shape.id, - currentPageId, - hasUnlockedShape: !shape.isLocked, - type: transformType, - initialShape: Utils.deepClone(shape), - initialShapeBounds: TLDR.getBounds(shape), - commonBounds: TLDR.getRotatedBounds(shape), - } -} - -export type TransformSingleSnapshot = ReturnType diff --git a/packages/tldraw/src/state/sessions/TransformSingleSession/index.ts b/packages/tldraw/src/state/sessions/TransformSingleSession/index.ts index d5bf394d3..ebc1f82bd 100644 --- a/packages/tldraw/src/state/sessions/TransformSingleSession/index.ts +++ b/packages/tldraw/src/state/sessions/TransformSingleSession/index.ts @@ -1 +1 @@ -export * from './TransformSingleSession.session' +export * from './TransformSingleSession' diff --git a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts index 8669d2c5b..6f56b6a83 100644 --- a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts +++ b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts @@ -1,328 +1,317 @@ -import { TLDrawState } from '~state' -import { mockDocument } from '~test' -import { GroupShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { GroupShape, SessionType, TDShapeType, TDStatus } from '~types' describe('Translate session', () => { - const state = new TLDrawState() - it('begins, updateSession', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.Translate, [5, 5]) - .updateSession([10, 10]) + .pointShape('rect1', [5, 5]) + .movePointer([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) - state.completeSession() + app.completeSession() - expect(state.appState.status).toBe(TLDrawStatus.Idle) + expect(app.status).toBe(TDStatus.Idle) - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) - state.undo() + app.undo() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) - state.redo() + app.redo() - expect(state.getShape('rect1').point).toStrictEqual([5, 5]) + expect(app.getShape('rect1').point).toStrictEqual([5, 5]) }) it('cancels session', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Translate, [5, 5]) - .updateSession([10, 10]) + .pointBounds([5, 5]) + .movePointer([10, 10]) .cancelSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) }) it('moves a single shape', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20]) + .pointShape('rect1', [10, 10]) + .movePointer([20, 20]) .completeSession() - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) }) it('moves a single shape along a locked axis', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], true) + .pointShape('rect1', [10, 10]) + .movePointer({ x: 20, y: 20, shiftKey: true }) .completeSession() - expect(state.getShape('rect1').point).toStrictEqual([10, 0]) + expect(app.getShape('rect1').point).toStrictEqual([10, 0]) }) it('moves two shapes', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20]) + .pointBounds([10, 10]) + .movePointer([20, 20]) .completeSession() - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) - expect(state.getShape('rect2').point).toStrictEqual([110, 110]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect2').point).toStrictEqual([110, 110]) }) it('undos and redos clones', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + .pointBounds([10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) - expect(Object.keys(state.getPage().shapes).length).toBe(5) + expect(Object.keys(app.getPage().shapes).length).toBe(5) - state.undo() + app.undo() - expect(Object.keys(state.getPage().shapes).length).toBe(3) + expect(Object.keys(app.getPage().shapes).length).toBe(3) - state.redo() + app.redo() - expect(Object.keys(state.getPage().shapes).length).toBe(5) + expect(Object.keys(app.getPage().shapes).length).toBe(5) }) it('clones shapes', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + .pointBounds([10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) - expect(Object.keys(state.getPage().shapes).length).toBe(5) + expect(Object.keys(app.getPage().shapes).length).toBe(5) }) it('destroys clones when last update is not cloning', () => { - state.resetDocument().loadDocument(mockDocument) + const app = new TldrawTestApp().loadDocument(mockDocument) - expect(Object.keys(state.getPage().shapes).length).toBe(3) + expect(Object.keys(app.getPage().shapes).length).toBe(3) - state - .select('rect1', 'rect2') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + app.select('rect1', 'rect2').pointBounds([10, 10]).movePointer({ x: 20, y: 20, altKey: true }) - expect(Object.keys(state.getPage().shapes).length).toBe(5) + expect(Object.keys(app.getPage().shapes).length).toBe(5) - state.updateSession([30, 30], false, false) + app.movePointer({ x: 30, y: 30 }) - expect(Object.keys(state.getPage().shapes).length).toBe(3) + expect(Object.keys(app.getPage().shapes).length).toBe(3) - state.completeSession() + app.completeSession() // Original position + delta - expect(state.getShape('rect1').point).toStrictEqual([30, 30]) - expect(state.getShape('rect2').point).toStrictEqual([130, 130]) + expect(app.getShape('rect1').point).toStrictEqual([30, 30]) + expect(app.getShape('rect2').point).toStrictEqual([130, 130]) - expect(Object.keys(state.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3']) + expect(Object.keys(app.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3']) }) it('destroys bindings from the translating shape', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectAll() .delete() .createShapes( { id: 'target1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, parentId: 'page1', point: [0, 0], size: [100, 100], }, { id: 'arrow1', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, parentId: 'page1', point: [200, 200], } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer([200, 200]) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer([50, 50]) .completeSession() - expect(state.bindings.length).toBe(1) + expect(app.bindings.length).toBe(1) - state - .select('arrow1') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([30, 30]) - .completeSession() + app.pointShape('arrow1', [10, 10]).movePointer([30, 30]).completeSession() - // expect(state.bindings.length).toBe(0) - // expect(state.getShape('arrow1').handles.start.bindingId).toBe(undefined) + // expect(app.bindings.length).toBe(0) + // expect(app.getShape('arrow1').handles.start.bindingId).toBe(undefined) - // state.undo() + // app.undo() - // expect(state.bindings.length).toBe(1) - // expect(state.getShape('arrow1').handles.start.bindingId).toBeTruthy() + // expect(app.bindings.length).toBe(1) + // expect(app.getShape('arrow1').handles.start.bindingId).toBeTruthy() - // state.redo() + // app.redo() - // expect(state.bindings.length).toBe(0) - // expect(state.getShape('arrow1').handles.start.bindingId).toBe(undefined) + // expect(app.bindings.length).toBe(0) + // expect(app.getShape('arrow1').handles.start.bindingId).toBe(undefined) }) // it.todo('clones a shape with a parent shape') describe('when translating a child of a group', () => { it('translates the shape and updates the groups size / point', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') .group(['rect1', 'rect2'], 'groupA') - .select('rect1') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, false) + .pointShape('rect1', [10, 10]) + .movePointer({ x: 20, y: 20 }) .completeSession() - expect(state.getShape('groupA').point).toStrictEqual([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('groupA').point).toStrictEqual([10, 10]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect2').point).toStrictEqual([110, 110]) - state.undo() + app.undo() - expect(state.getShape('groupA').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('groupA').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) - state.redo() + app.redo() - expect(state.getShape('groupA').point).toStrictEqual([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('groupA').point).toStrictEqual([10, 10]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect2').point).toStrictEqual([110, 110]) }) it('clones the shape and updates the parent', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') .group(['rect1', 'rect2'], 'groupA') - .select('rect1') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + .doubleClickShape('rect1') + .pointShape('rect1', [10, 10]) + .movePointer({ x: 10, y: 10, altKey: true }) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() - const children = state.getShape('groupA').children + const children = app.getShape('groupA').children const newShapeId = children[children.length - 1] - expect(state.getShape('groupA').point).toStrictEqual([0, 0]) - expect(state.getShape('groupA').children.length).toBe(3) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) - expect(state.getShape(newShapeId).point).toStrictEqual([20, 20]) - expect(state.getShape(newShapeId).parentId).toBe('groupA') + expect(app.getShape('groupA').point).toStrictEqual([0, 0]) + expect(app.getShape('groupA').children.length).toBe(3) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape(newShapeId).point).toStrictEqual([20, 20]) + expect(app.getShape(newShapeId).parentId).toBe('groupA') - state.undo() + app.undo() - expect(state.getShape('groupA').point).toStrictEqual([0, 0]) - expect(state.getShape('groupA').children.length).toBe(2) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) - expect(state.getShape(newShapeId)).toBeUndefined() + expect(app.getShape('groupA').point).toStrictEqual([0, 0]) + expect(app.getShape('groupA').children.length).toBe(2) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape(newShapeId)).toBeUndefined() - state.redo() + app.redo() - expect(state.getShape('groupA').point).toStrictEqual([0, 0]) - expect(state.getShape('groupA').children.length).toBe(3) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) - expect(state.getShape(newShapeId).point).toStrictEqual([20, 20]) - expect(state.getShape(newShapeId).parentId).toBe('groupA') + expect(app.getShape('groupA').point).toStrictEqual([0, 0]) + expect(app.getShape('groupA').children.length).toBe(3) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape(newShapeId).point).toStrictEqual([20, 20]) + expect(app.getShape(newShapeId).parentId).toBe('groupA') }) }) describe('when translating a shape with children', () => { it('translates the shapes children', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') .group(['rect1', 'rect2'], 'groupA') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, false) + .pointShape('groupA', [10, 10]) + .movePointer({ x: 20, y: 20 }) .completeSession() - expect(state.getShape('groupA').point).toStrictEqual([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) - expect(state.getShape('rect2').point).toStrictEqual([110, 110]) + expect(app.getShape('groupA').point).toStrictEqual([10, 10]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect2').point).toStrictEqual([110, 110]) - state.undo() + app.undo() - expect(state.getShape('groupA').point).toStrictEqual([0, 0]) - expect(state.getShape('rect1').point).toStrictEqual([0, 0]) - expect(state.getShape('rect2').point).toStrictEqual([100, 100]) + expect(app.getShape('groupA').point).toStrictEqual([0, 0]) + expect(app.getShape('rect1').point).toStrictEqual([0, 0]) + expect(app.getShape('rect2').point).toStrictEqual([100, 100]) - state.redo() + app.redo() - expect(state.getShape('groupA').point).toStrictEqual([10, 10]) - expect(state.getShape('rect1').point).toStrictEqual([10, 10]) - expect(state.getShape('rect2').point).toStrictEqual([110, 110]) + expect(app.getShape('groupA').point).toStrictEqual([10, 10]) + expect(app.getShape('rect1').point).toStrictEqual([10, 10]) + expect(app.getShape('rect2').point).toStrictEqual([110, 110]) }) it('clones the shapes and children', () => { - state + new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .group() - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + .group(['rect1', 'rect2'], 'groupA') + .pointShape('groupA', [10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() }) it('deletes clones when not cloning anymore', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .group() - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) - .updateSession([20, 20], false, false) - .updateSession([20, 20], false, true) + .group(['rect1', 'rect2'], 'groupA') + .pointShape('groupA', [10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) + .movePointer({ x: 20, y: 20, altKey: false }) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() - expect(state.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(2) + expect(app.shapes.filter((shape) => shape.type === TDShapeType.Group).length).toBe(2) }) it('deletes clones when not cloning anymore', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') - .group() - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) - .updateSession([20, 20], false, false) + .group(['rect1', 'rect2'], 'groupA') + .pointShape('groupA', [10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) + .movePointer({ x: 20, y: 20, altKey: false }) .completeSession() - expect(state.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(1) + expect(app.shapes.filter((shape) => shape.type === TDShapeType.Group).length).toBe(1) }) it('clones the shapes and children when selecting a group and a different shape', () => { - state + const app = new TldrawTestApp() .loadDocument(mockDocument) .select('rect1', 'rect2') .group(['rect1', 'rect2'], 'groupA') .select('groupA', 'rect3') - .startSession(SessionType.Translate, [10, 10]) - .updateSession([20, 20], false, true) + .pointBounds([10, 10]) + .movePointer({ x: 20, y: 20, altKey: true }) .completeSession() }) }) @@ -330,15 +319,17 @@ describe('Translate session', () => { describe('When creating with a translate session', () => { it('Deletes the shape on undo', () => { - const state = new TLDrawState() - .loadDocument(mockDocument) - .select('rect1') - .startSession(SessionType.Translate, [5, 5], true) - .updateSession([10, 10]) + const app = new TldrawTestApp() + .selectTool(TDShapeType.Rectangle) + .pointCanvas([0, 0]) + .movePointer([10, 10]) .completeSession() - .undo() - expect(state.getShape('rect1')).toBe(undefined) + expect(app.shapes.length).toBe(1) + + app.undo() + + expect(app.shapes.length).toBe(0) }) }) diff --git a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts index 0845086fb..a064b0dff 100644 --- a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts +++ b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts @@ -2,20 +2,21 @@ import { TLPageState, Utils, TLBoundsWithCenter, TLSnapLine, TLBounds } from '@tldraw/core' import { Vec } from '@tldraw/vec' import { - TLDrawShape, - TLDrawBinding, - Session, - TLDrawSnapshot, - TLDrawCommand, - TLDrawStatus, + TDShape, + TDBinding, + TldrawCommand, + TDStatus, ArrowShape, GroupShape, SessionType, ArrowBinding, + TldrawPatch, } from '~types' import { SLOW_SPEED, SNAP_DISTANCE } from '~constants' import { TLDR } from '~state/TLDR' import type { Patch } from 'rko' +import { BaseSession } from '../BaseSession' +import type { TldrawApp } from '../../internal' type CloneInfo = | { @@ -23,7 +24,7 @@ type CloneInfo = } | { state: 'ready' - clones: TLDrawShape[] + clones: TDShape[] clonedBindings: ArrowBinding[] } @@ -37,15 +38,13 @@ type SnapInfo = bounds: TLBoundsWithCenter[] } -export class TranslateSession extends Session { +export class TranslateSession extends BaseSession { type = SessionType.Translate - status = TLDrawStatus.Translating + status = TDStatus.Translating delta = [0, 0] prev = [0, 0] prevPoint = [0, 0] speed = 1 - origin: number[] - snapshot: TranslateSnapshot cloneInfo: CloneInfo = { state: 'empty', } @@ -55,38 +54,109 @@ export class TranslateSession extends Session { snapLines: TLSnapLine[] = [] isCloning = false isCreate: boolean - isLinked: 'left' | 'right' | 'center' | false + link: 'left' | 'right' | 'center' | false - constructor( - data: TLDrawSnapshot, - viewport: TLBounds, - point: number[], - isCreate = false, - isLinked: 'left' | 'right' | 'center' | false = false - ) { - super(viewport) - this.origin = point - this.snapshot = getTranslateSnapshot(data, isLinked) + initialIds: Set + hasUnlockedShapes: boolean + initialSelectedIds: string[] + initialCommonBounds: TLBounds + initialShapes: TDShape[] + initialParentChildren: Record + bindingsToDelete: ArrowBinding[] + + constructor(app: TldrawApp, isCreate = false, link: 'left' | 'right' | 'center' | false = false) { + super(app) this.isCreate = isCreate - this.isLinked = isLinked - Session.cache.selectedIds = [...TLDR.getSelectedIds(data, data.appState.currentPageId)] + this.link = link + + const { currentPageId, selectedIds, page } = this.app + + this.initialSelectedIds = [...selectedIds] + + const selectedShapes = ( + link ? TLDR.getLinkedShapeIds(this.app.state, currentPageId, link, false) : selectedIds + ) + .map((id) => this.app.getShape(id)) + .filter((shape) => !shape.isLocked) + + const selectedShapeIds = new Set(selectedShapes.map((shape) => shape.id)) + + this.hasUnlockedShapes = selectedShapes.length > 0 + + this.initialShapes = Array.from( + new Set( + selectedShapes + .filter((shape) => !selectedShapeIds.has(shape.parentId)) + .flatMap((shape) => { + return shape.children + ? [shape, ...shape.children.map((childId) => this.app.getShape(childId))] + : [shape] + }) + ).values() + ) + + this.initialIds = new Set(this.initialShapes.map((shape) => shape.id)) + + this.bindingsToDelete = [] + + Object.values(page.bindings) + .filter((binding) => this.initialIds.has(binding.fromId) || this.initialIds.has(binding.toId)) + .forEach((binding) => { + if (this.initialIds.has(binding.fromId)) { + if (!this.initialIds.has(binding.toId)) { + this.bindingsToDelete.push(binding) + } + } + }) + + this.initialParentChildren = {} + + this.initialShapes + .map((s) => s.parentId) + .filter((id) => id !== page.id) + .forEach((id) => { + this.initialParentChildren[id] = this.app.getShape(id).children! + }) + + this.initialCommonBounds = Utils.getCommonBounds(this.initialShapes.map(TLDR.getRotatedBounds)) + + this.app.rotationInfo.selectedIds = [...this.app.selectedIds] } - start = (data: TLDrawSnapshot) => { - const { bindingsToDelete } = this.snapshot + start = (): TldrawPatch | undefined => { + const { + bindingsToDelete, + initialIds, + app: { currentPageId, page }, + } = this - this.createSnapInfo(data) + const allBounds: TLBoundsWithCenter[] = [] + const otherBounds: TLBoundsWithCenter[] = [] - if (bindingsToDelete.length === 0) return data + Object.values(page.shapes).forEach((shape) => { + const bounds = Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape)) + allBounds.push(bounds) + if (!initialIds.has(shape.id)) { + otherBounds.push(bounds) + } + }) - const nextBindings: Patch> = {} + this.snapInfo = { + state: 'ready', + bounds: allBounds, + others: otherBounds, + } + + if (bindingsToDelete.length === 0) return + + const nextBindings: Patch> = {} bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined)) return { document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { bindings: nextBindings, }, }, @@ -94,24 +164,34 @@ export class TranslateSession extends Session { } } - update = ( - data: TLDrawSnapshot, - point: number[], - shiftKey = false, - altKey = false, - metaKey = false - ) => { - const { selectedIds, initialParentChildren, initialShapes, bindingsToDelete } = this.snapshot + update = (): TldrawPatch | undefined => { + const { + initialParentChildren, + initialShapes, + initialCommonBounds, + bindingsToDelete, + app: { + pageState: { camera }, + settings: { isSnapping }, + currentPageId, + viewport, + selectedIds, + currentPoint, + previousPoint, + originPoint, + altKey, + shiftKey, + metaKey, + }, + } = this - const { currentPageId } = data.appState + const nextBindings: Patch> = {} - const nextBindings: Patch> = {} - - const nextShapes: Patch> = {} + const nextShapes: Patch> = {} const nextPageState: Patch = {} - let delta = Vec.sub(point, this.origin) + let delta = Vec.sub(currentPoint, originPoint) let didChangeCloning = false @@ -143,9 +223,7 @@ export class TranslateSession extends Session { // speed, but we also want the speed to accelerate faster than // it decelerates. - const speed = Vec.dist(point, this.prevPoint) - - this.prevPoint = point + const speed = Vec.dist(currentPoint, previousPoint) const change = speed - this.speed @@ -153,24 +231,19 @@ export class TranslateSession extends Session { this.snapLines = [] - const { zoom } = data.document.pageStates[currentPageId].camera - if ( - ((data.settings.isSnapping && !metaKey) || (!data.settings.isSnapping && metaKey)) && - this.speed * zoom < SLOW_SPEED && + ((isSnapping && !metaKey) || (!isSnapping && metaKey)) && + this.speed * camera.zoom < SLOW_SPEED && this.snapInfo.state === 'ready' ) { - const bounds = Utils.getBoundsWithCenter( - Utils.translateBounds(this.snapshot.commonBounds, delta) - ) + const bounds = Utils.getBoundsWithCenter(Utils.translateBounds(initialCommonBounds, delta)) const snapResult = Utils.getSnapPoints( bounds, (this.isCloning ? this.snapInfo.bounds : this.snapInfo.others).filter( - (bounds) => - Utils.boundsContain(this.viewport, bounds) || Utils.boundsCollide(this.viewport, bounds) + (bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds) ), - SNAP_DISTANCE / zoom + SNAP_DISTANCE / camera.zoom ) if (snapResult) { @@ -195,7 +268,7 @@ export class TranslateSession extends Session { // Not Cloning -> Cloning if (didChangeCloning) { if (this.cloneInfo.state === 'empty') { - this.createCloneInfo(data) + this.createCloneInfo() } if (this.cloneInfo.state === 'empty') { @@ -217,10 +290,7 @@ export class TranslateSession extends Session { nextShapes[clone.id] = { ...clone, point: Vec.round(Vec.add(clone.point, delta)) } // Add clones to non-selected parents - if ( - clone.parentId !== data.appState.currentPageId && - !selectedIds.includes(clone.parentId) - ) { + if (clone.parentId !== currentPageId && !selectedIds.includes(clone.parentId)) { const children = nextShapes[clone.parentId]?.children || initialParentChildren[clone.parentId] @@ -248,8 +318,7 @@ export class TranslateSession extends Session { // Either way, move the clones clones.forEach((clone) => { - const current = (nextShapes[clone.id] || - TLDR.getShape(data, clone.id, data.appState.currentPageId)) as TLDrawShape + const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape if (!current.point) throw Error('No point on that clone!') @@ -304,8 +373,7 @@ export class TranslateSession extends Session { // Move the shapes by the delta initialShapes.forEach((shape) => { - const current = (nextShapes[shape.id] || - TLDR.getShape(data, shape.id, data.appState.currentPageId)) as TLDrawShape + const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape if (!current.point) throw Error('No point on that clone!') @@ -322,23 +390,28 @@ export class TranslateSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: nextShapes, bindings: nextBindings, }, }, pageStates: { - [data.appState.currentPageId]: nextPageState, + [currentPageId]: nextPageState, }, }, } } - cancel = (data: TLDrawSnapshot) => { - const { initialShapes, bindingsToDelete } = this.snapshot + cancel = (): TldrawPatch | undefined => { + const { + initialShapes, + initialSelectedIds, + bindingsToDelete, + app: { currentPageId }, + } = this - const nextBindings: Record | undefined> = {} - const nextShapes: Record | undefined> = {} + const nextBindings: Record | undefined> = {} + const nextShapes: Record | undefined> = {} const nextPageState: Partial = { editingId: undefined, hoveredId: undefined, @@ -353,7 +426,7 @@ export class TranslateSession extends Session { } else { // Put initial shapes back to where they started initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point })) - nextPageState.selectedIds = this.snapshot.selectedIds + nextPageState.selectedIds = initialSelectedIds } if (this.cloneInfo.state === 'ready') { @@ -371,32 +444,35 @@ export class TranslateSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: nextShapes, bindings: nextBindings, }, }, pageStates: { - [data.appState.currentPageId]: nextPageState, + [currentPageId]: nextPageState, }, }, } } - complete = (data: TLDrawSnapshot): TLDrawCommand => { - const pageId = data.appState.currentPageId + complete = (): TldrawPatch | TldrawCommand | undefined => { + const { + initialShapes, + initialParentChildren, + bindingsToDelete, + app: { currentPageId }, + } = this - const { initialShapes, initialParentChildren, bindingsToDelete } = this.snapshot + const beforeBindings: Patch> = {} + const beforeShapes: Patch> = {} - const beforeBindings: Patch> = {} - const beforeShapes: Patch> = {} - - const afterBindings: Patch> = {} - const afterShapes: Patch> = {} + const afterBindings: Patch> = {} + const afterShapes: Patch> = {} if (this.isCloning) { if (this.cloneInfo.state === 'empty') { - this.createCloneInfo(data) + this.createCloneInfo() } if (this.cloneInfo.state !== 'ready') throw Error @@ -406,9 +482,9 @@ export class TranslateSession extends Session { clones.forEach((clone) => { beforeShapes[clone.id] = undefined - afterShapes[clone.id] = TLDR.getShape(data, clone.id, pageId) + afterShapes[clone.id] = this.app.getShape(clone.id) - if (clone.parentId !== pageId) { + if (clone.parentId !== currentPageId) { beforeShapes[clone.parentId] = { ...beforeShapes[clone.parentId], children: initialParentChildren[clone.parentId], @@ -416,7 +492,7 @@ export class TranslateSession extends Session { afterShapes[clone.parentId] = { ...afterShapes[clone.parentId], - children: TLDR.getShape(data, clone.parentId, pageId).children, + children: this.app.getShape(clone.parentId).children, } } }) @@ -424,7 +500,7 @@ export class TranslateSession extends Session { // Update the cloned bindings clonedBindings.forEach((binding) => { beforeBindings[binding.id] = undefined - afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId) + afterBindings[binding.id] = this.app.getBinding(binding.id) }) } else { // If we aren't cloning, then update the initial shapes @@ -439,8 +515,8 @@ export class TranslateSession extends Session { afterShapes[shape.id] = { ...afterShapes[shape.id], ...(this.isCreate - ? TLDR.getShape(data, shape.id, pageId) - : { point: TLDR.getShape(data, shape.id, pageId).point }), + ? this.app.getShape(shape.id) + : { point: this.app.getShape(shape.id).point }), } }) } @@ -451,7 +527,7 @@ export class TranslateSession extends Session { for (const id of [binding.toId, binding.fromId]) { // Let's also look at the bound shape... - const shape = TLDR.getShape(data, id, pageId) + const shape = this.app.getShape(id) // If the bound shape has a handle that references the deleted binding, delete that reference if (!shape.handles) continue @@ -486,14 +562,14 @@ export class TranslateSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: beforeShapes, bindings: beforeBindings, }, }, pageStates: { - [data.appState.currentPageId]: { - selectedIds: this.isCreate ? [] : [...this.snapshot.selectedIds], + [currentPageId]: { + selectedIds: this.isCreate ? [] : [...this.initialSelectedIds], }, }, }, @@ -504,14 +580,14 @@ export class TranslateSession extends Session { }, document: { pages: { - [data.appState.currentPageId]: { + [currentPageId]: { shapes: afterShapes, bindings: afterBindings, }, }, pageStates: { - [data.appState.currentPageId]: { - selectedIds: [...TLDR.getSelectedIds(data, data.appState.currentPageId)], + [currentPageId]: { + selectedIds: [...this.app.selectedIds], }, }, }, @@ -519,45 +595,24 @@ export class TranslateSession extends Session { } } - private createSnapInfo = async (data: TLDrawSnapshot) => { - const { currentPageId } = data.appState - const page = data.document.pages[currentPageId] - const { idsToMove } = this.snapshot - - const allBounds: TLBoundsWithCenter[] = [] - const otherBounds: TLBoundsWithCenter[] = [] - - Object.values(page.shapes).forEach((shape) => { - const bounds = Utils.getBoundsWithCenter(TLDR.getRotatedBounds(shape)) - allBounds.push(bounds) - if (!idsToMove.has(shape.id)) { - otherBounds.push(bounds) - } - }) - - this.snapInfo = { - state: 'ready', - bounds: allBounds, - others: otherBounds, - } - } - - private createCloneInfo = (data: TLDrawSnapshot) => { + private createCloneInfo = () => { // Create clones when as they're needed. // Consider doing this work in a worker. - const { currentPageId } = data.appState - const page = data.document.pages[currentPageId] - const { selectedIds, shapesToMove, initialParentChildren } = this.snapshot + const { + initialShapes, + initialParentChildren, + app: { selectedIds, currentPageId, page }, + } = this const cloneMap: Record = {} const clonedBindingsMap: Record = {} - const clonedBindings: TLDrawBinding[] = [] + const clonedBindings: TDBinding[] = [] // Create clones of selected shapes - const clones: TLDrawShape[] = [] + const clones: TDShape[] = [] - shapesToMove.forEach((shape) => { + initialShapes.forEach((shape) => { const newId = Utils.uniqueId() initialParentChildren[newId] = initialParentChildren[shape.id] @@ -568,7 +623,7 @@ export class TranslateSession extends Session { ...Utils.deepClone(shape), id: newId, parentId: shape.parentId, - childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId), + childIndex: TLDR.getChildIndexAbove(this.app.state, shape.id, currentPageId), } clones.push(clone) @@ -598,14 +653,12 @@ export class TranslateSession extends Session { if (clonedShapeIds.has(binding.fromId)) { if (clonedShapeIds.has(binding.toId)) { const cloneId = Utils.uniqueId() - const cloneBinding = { ...Utils.deepClone(binding), id: cloneId, fromId: cloneMap[binding.fromId] || binding.fromId, toId: cloneMap[binding.toId] || binding.toId, } - clonedBindingsMap[binding.id] = cloneId clonedBindings.push(cloneBinding) } @@ -637,73 +690,3 @@ export class TranslateSession extends Session { } } } - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function getTranslateSnapshot( - data: TLDrawSnapshot, - linkDirection: 'left' | 'right' | 'center' | false -) { - const { currentPageId } = data.appState - - const page = TLDR.getPage(data, currentPageId) - - const selectedIds = TLDR.getSelectedIds(data, currentPageId) - - const ids = linkDirection - ? TLDR.getLinkedShapes(data, currentPageId, linkDirection, false) - : selectedIds - - const selectedShapes = ids.flatMap((id) => TLDR.getShape(data, id, currentPageId)) - - const hasUnlockedShapes = selectedShapes.length > 0 - - const shapesToMove: TLDrawShape[] = selectedShapes - .filter((shape) => !ids.includes(shape.parentId)) - .flatMap((shape) => { - return shape.children - ? [shape, ...shape.children!.map((childId) => TLDR.getShape(data, childId, currentPageId))] - : [shape] - }) - - const idsToMove = new Set(shapesToMove.map((shape) => shape.id)) - - const bindingsToDelete: ArrowBinding[] = [] - - Object.values(page.bindings) - .filter((binding) => idsToMove.has(binding.fromId) || idsToMove.has(binding.toId)) - .forEach((binding) => { - if (idsToMove.has(binding.fromId)) { - if (!idsToMove.has(binding.toId)) { - bindingsToDelete.push(binding) - } - } - }) - - const initialParentChildren: Record = {} - - Array.from(new Set(shapesToMove.map((s) => s.parentId)).values()) - .filter((id) => id !== page.id) - .forEach((id) => { - const shape = TLDR.getShape(data, id, currentPageId) - initialParentChildren[id] = shape.children! - }) - - const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getRotatedBounds)) - - return { - selectedIds, - hasUnlockedShapes, - initialParentChildren, - idsToMove, - shapesToMove, - bindingsToDelete, - commonBounds, - initialShapes: shapesToMove.map(({ id, point, parentId }) => ({ - id, - point, - parentId, - })), - } -} - -export type TranslateSnapshot = ReturnType diff --git a/packages/tldraw/src/state/sessions/about-sessions.md b/packages/tldraw/src/state/sessions/about-sessions.md index 129f9b455..4e22d27f9 100644 --- a/packages/tldraw/src/state/sessions/about-sessions.md +++ b/packages/tldraw/src/state/sessions/about-sessions.md @@ -4,11 +4,11 @@ A session is a class that handles events for interactions that have a beginning, They contrast with Commands, such as `duplicate`, which occur once. -The `TLDrawState` may only have one active session at a time (`TLDrawState.session`), or it may have no session. It may never have two sessions simulataneously—if a session begins while another session is already in progress, `TLDrawState` will throw an error. In this way, sessions function similar to a set of finite states: once a session begins, it must end before a new session can begin. +The `tldrawApp` may only have one active session at a time (`tldrawApp.session`), or it may have no session. It may never have two sessions simulataneously—if a session begins while another session is already in progress, `tldrawApp` will throw an error. In this way, sessions function similar to a set of finite states: once a session begins, it must end before a new session can begin. ## Creating a Session -Sessions are created with `TLDrawState.startSession`. In this method, sessions are creating using the `new` keyword. Every session's constructor receives the `TLDrawState` instance's current state (`TLDrawState.state`), together with any additional parameters it defines in its constructor. +Sessions are created with `tldrawApp.startSession`. In this method, sessions are creating using the `new` keyword. Every session's constructor receives the `tldrawApp` instance's current state (`tldrawApp.state`), together with any additional parameters it defines in its constructor. ## Life Cycle Methods @@ -16,23 +16,23 @@ A session has four life-cycle methods: `start`, `update`, `cancel` and `complete ### Start -When a session is created using `TLDrawState.startSession`, `TLDrawState` also calls the session's `start` method, passing in the state as the only parameter. If the `start` method returns a patch, then that patch is applied to the state. +When a session is created using `tldrawApp.startSession`, `tldrawApp` also calls the session's `start` method, passing in the state as the only parameter. If the `start` method returns a patch, then that patch is applied to the state. ### Update -When a session is updated using `TLDrawState.updateSession`, `TLDrawState` calls the session's `update` method, again passing in the state as well as several additional parameters: `point`, `shiftKey`, `altKey`, and `metaKey`. If the `update` method returns a patch, then that patch is applied to the state. +When a session is updated using `tldrawApp.updateSession`, `tldrawApp` calls the session's `update` method, again passing in the state as well as several additional parameters: `point`, `shiftKey`, `altKey`, and `metaKey`. If the `update` method returns a patch, then that patch is applied to the state. A session may use whatever information is wishes internally in order to produce its update patch. Often this means saving information about the initial state, point, or initial selected shapes, in order to compare against the update's parameters. For example, `RotateSession.update` saves the center of the selection bounds, as well as the initial angle from this center to the user's initial point, in order to compare this angle against the angle from this center to the user's current point. ### Cancel -A session may be cancelled using `TLDrawState.cancelSession`. When a session is cancelled, `TLDrawState` calls the session's `cancel` method passing in the state as the only parameter. If the `cancel` method returns a patch, then that patch is applied to the state. +A session may be cancelled using `tldrawApp.cancelSession`. When a session is cancelled, `tldrawApp` calls the session's `cancel` method passing in the state as the only parameter. If the `cancel` method returns a patch, then that patch is applied to the state. A cancel method is expected to revert any changes made to the state since the session began. For example, `RotateSession.cancel` should restore the rotations of the user's selected shapes to their original rotations. If no change has occurred (e.g. if the rotation began and was immediately cancelled) then the `cancel` method should return `undefined` so as to avoid updating the state. ### Complete -A session may be cancelled using `TLDrawState.complete`. When a session is cancelled, `TLDrawState` calls the session's `complete` method passing in the state as the only parameter. If the `complete` method returns a patch, then that patch is applied to the state; if it returns a `command`, then that command is patched and added to the state's history. +A session may be cancelled using `tldrawApp.complete`. When a session is cancelled, `tldrawApp` calls the session's `complete` method passing in the state as the only parameter. If the `complete` method returns a patch, then that patch is applied to the state; if it returns a `command`, then that command is patched and added to the state's history. If the `complete` method returns a command, then it is expected that the command's `before` patch will revert any changes made to the state since the session began, including any changes introduced in the command's `after` patch. diff --git a/packages/tldraw/src/state/sessions/index.ts b/packages/tldraw/src/state/sessions/index.ts index c91e84194..e0c0f6ee0 100644 --- a/packages/tldraw/src/state/sessions/index.ts +++ b/packages/tldraw/src/state/sessions/index.ts @@ -1,4 +1,4 @@ -import { SessionType } from '~types' +import { ExceptFirst, SessionType } from '~types' import { ArrowSession } from './ArrowSession' import { BrushSession } from './BrushSession' import { DrawSession } from './DrawSession' @@ -7,12 +7,26 @@ import { RotateSession } from './RotateSession' import { TransformSession } from './TransformSession' import { TransformSingleSession } from './TransformSingleSession' import { TranslateSession } from './TranslateSession' +import { EraseSession } from './EraseSession' import { GridSession } from './GridSession' +export type TldrawSession = + | ArrowSession + | BrushSession + | DrawSession + | HandleSession + | RotateSession + | TransformSession + | TransformSingleSession + | TranslateSession + | EraseSession + | GridSession + export interface SessionsMap { [SessionType.Arrow]: typeof ArrowSession [SessionType.Brush]: typeof BrushSession [SessionType.Draw]: typeof DrawSession + [SessionType.Erase]: typeof EraseSession [SessionType.Handle]: typeof HandleSession [SessionType.Rotate]: typeof RotateSession [SessionType.Transform]: typeof TransformSession @@ -23,12 +37,15 @@ export interface SessionsMap { export type SessionOfType = SessionsMap[K] -export type ArgsOfType = ConstructorParameters> +export type SessionArgsOfType = ExceptFirst< + ConstructorParameters> +> export const sessions: { [K in SessionType]: SessionsMap[K] } = { [SessionType.Arrow]: ArrowSession, [SessionType.Brush]: BrushSession, [SessionType.Draw]: DrawSession, + [SessionType.Erase]: EraseSession, [SessionType.Handle]: HandleSession, [SessionType.Rotate]: RotateSession, [SessionType.Transform]: TransformSession, diff --git a/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx b/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx index a08ef6e41..b4938e897 100644 --- a/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx +++ b/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx @@ -1,26 +1,27 @@ import * as React from 'react' import { Utils, TLBounds, TLPointerInfo, SVGContainer } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { defaultStyle, getShapeStyle } from '../shape-styles' +import { defaultStyle, getShapeStyle } from '../shared/shape-styles' import { ArrowShape, - TLDrawTransformInfo, + TransformInfo, Decoration, - TLDrawShapeType, - TLDrawShape, + TDShapeType, + TDShape, EllipseShape, - TLDrawBinding, + TDBinding, DashStyle, - TLDrawMeta, + TDMeta, } from '~types' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' +import { TDShapeUtil } from '../TDShapeUtil' import { intersectArcBounds, intersectLineSegmentBounds, + intersectLineSegmentLineSegment, intersectRayBounds, intersectRayEllipse, } from '@tldraw/intersect' -import { BINDING_DISTANCE, EASINGS } from '~constants' +import { BINDING_DISTANCE, EASINGS, GHOSTED_OPACITY } from '~constants' import { getArcPoints, getArrowArc, @@ -38,8 +39,8 @@ import { type T = ArrowShape type E = SVGSVGElement -export class ArrowUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Arrow as const +export class ArrowUtil extends TDShapeUtil { + type = TDShapeType.Arrow as const hideBounds = true @@ -49,7 +50,7 @@ export class ArrowUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, name: 'Arrow', parentId: 'page', childIndex: 1, @@ -87,7 +88,7 @@ export class ArrowUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component(({ shape, meta, events }, ref) => { + Component = TDShapeUtil.Component(({ shape, isGhost, meta, events }, ref) => { const { handles: { start, bend, end }, decorations = {}, @@ -232,7 +233,7 @@ export class ArrowUtil extends TLDrawShapeUtil { return ( - + {shaftPath} {startArrowHead && ( { ) }) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { return }) getBounds = (shape: T) => { const bounds = Utils.getFromCache(this.boundsCache, shape, () => { - const points = getArcPoints(shape) - return Utils.getBoundsFromPoints(points) + return Utils.getBoundsFromPoints(getArcPoints(shape)) }) return Utils.translateBounds(bounds, shape.point) @@ -305,6 +305,34 @@ export class ArrowUtil extends TLDrawShapeUtil { ) } + hitTestPoint = (shape: T, point: number[]): boolean => { + const pt = Vec.sub(point, shape.point) + const points = getArcPoints(shape) + + for (let i = 1; i < points.length; i++) { + if (Vec.distanceToLineSegment(points[i - 1], points[i], pt) < 1) { + return true + } + } + + return false + } + + hitTestLineSegment = (shape: T, A: number[], B: number[]): boolean => { + const ptA = Vec.sub(A, shape.point) + const ptB = Vec.sub(B, shape.point) + + const points = getArcPoints(shape) + + for (let i = 1; i < points.length; i++) { + if (intersectLineSegmentLineSegment(points[i - 1], points[i], ptA, ptB).didIntersect) { + return true + } + } + + return false + } + hitTestBounds = (shape: T, bounds: TLBounds) => { const { start, end, bend } = shape.handles @@ -330,7 +358,7 @@ export class ArrowUtil extends TLDrawShapeUtil { transform = ( shape: T, bounds: TLBounds, - { initialShape, scaleX, scaleY }: TLDrawTransformInfo + { initialShape, scaleX, scaleY }: TransformInfo ): Partial => { const initialShapeBounds = this.getBounds(initialShape) @@ -414,8 +442,8 @@ export class ArrowUtil extends TLDrawShapeUtil { onBindingChange = ( shape: T, - binding: TLDrawBinding, - target: TLDrawShape, + binding: TDBinding, + target: TDShape, targetBounds: TLBounds, center: number[] ): Partial | void => { @@ -451,7 +479,7 @@ export class ArrowUtil extends TLDrawShapeUtil { // And passes through the dragging handle const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin)) - if (target.type === TLDrawShapeType.Ellipse) { + if (target.type === TDShapeType.Ellipse) { const hits = intersectRayEllipse( origin, direction, diff --git a/packages/tldraw/src/state/shapes/ArrowUtil/__snapshots__/ArrowUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/ArrowUtil/__snapshots__/ArrowUtil.spec.tsx.snap index 8d2a7a2a2..2fec41753 100644 --- a/packages/tldraw/src/state/shapes/ArrowUtil/__snapshots__/ArrowUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/ArrowUtil/__snapshots__/ArrowUtil.spec.tsx.snap @@ -47,6 +47,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "arrow", diff --git a/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts b/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts index 697df1b23..70e7903c2 100644 --- a/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts +++ b/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts @@ -3,12 +3,12 @@ import { intersectCircleCircle, intersectCircleLineSegment } from '@tldraw/inter import Vec from '@tldraw/vec' import getStroke from 'perfect-freehand' import { EASINGS } from '~constants' -import { getShapeStyle } from '../shape-styles' -import type { ArrowShape, TLDrawHandle } from '~types' +import { getShapeStyle } from '../shared/shape-styles' +import type { ArrowShape, TldrawHandle } from '~types' export function getArrowArcPath( - start: TLDrawHandle, - end: TLDrawHandle, + start: TldrawHandle, + end: TldrawHandle, circle: number[], bend: number ) { @@ -259,9 +259,9 @@ export function getArrowPath(shape: ArrowShape) { export function getArcPoints(shape: ArrowShape) { const { start, bend, end } = shape.handles - const points: number[][] = [start.point, end.point] - if (Vec.dist2(bend.point, Vec.med(start.point, end.point)) > 4) { + const points: number[][] = [] + // We're an arc, calculate points along the arc const { center, radius } = getArrowArc(shape) @@ -273,9 +273,11 @@ export function getArcPoints(shape: ArrowShape) { const angle = Utils.lerpAngles(startAngle, endAngle, i) points.push(Vec.nudgeAtAngle(center, angle, radius)) } - } - return points + return points + } else { + return [start.point, end.point] + } } export function isAngleBetween(a: number, b: number, c: number): boolean { diff --git a/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx b/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx index 497732fe8..c004d0769 100644 --- a/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx +++ b/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx @@ -1,21 +1,27 @@ import * as React from 'react' import { Utils, SVGContainer, TLBounds } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { defaultStyle, getShapeStyle } from '../shape-styles' -import { DrawShape, DashStyle, TLDrawShapeType, TLDrawTransformInfo, TLDrawMeta } from '~types' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' -import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect' +import { defaultStyle, getShapeStyle } from '../shared/shape-styles' +import { DrawShape, DashStyle, TDShapeType, TransformInfo, TDMeta } from '~types' +import { TDShapeUtil } from '../TDShapeUtil' import { - getDrawStrokePathTLDrawSnapshot, + intersectBoundsBounds, + intersectBoundsPolyline, + intersectLineSegmentBounds, + intersectLineSegmentLineSegment, +} from '@tldraw/intersect' +import { + getDrawStrokePathTDSnapshot, getFillPath, - getSolidStrokePathTLDrawSnapshot, + getSolidStrokePathTDSnapshot, } from './drawHelpers' +import { GHOSTED_OPACITY } from '~constants' type T = DrawShape type E = SVGSVGElement -export class DrawUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Draw as const +export class DrawUtil extends TDShapeUtil { + type = TDShapeType.Draw as const pointsBoundsCache = new WeakMap([]) @@ -29,7 +35,7 @@ export class DrawUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Draw, + type: TDShapeType.Draw, name: 'Draw', parentId: 'page', childIndex: 1, @@ -43,22 +49,21 @@ export class DrawUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component(({ shape, meta, events }, ref) => { + Component = TDShapeUtil.Component(({ shape, meta, isGhost, events }, ref) => { const { points, style, isComplete } = shape - const polygonPathTLDrawSnapshot = React.useMemo(() => { + const polygonPathTDSnapshot = React.useMemo(() => { return getFillPath(shape) }, [points, style.size]) - const pathTLDrawSnapshot = React.useMemo(() => { + const pathTDSnapshot = React.useMemo(() => { return style.dash === DashStyle.Draw - ? getDrawStrokePathTLDrawSnapshot(shape) - : getSolidStrokePathTLDrawSnapshot(shape) + ? getDrawStrokePathTDSnapshot(shape) + : getSolidStrokePathTDSnapshot(shape) }, [points, style.size, style.dash, isComplete]) const styles = getShapeStyle(style, meta.isDarkMode) - - const strokeWidth = styles.strokeWidth + const { stroke, fill, strokeWidth } = styles // For very short lines, draw a point instead of a line const bounds = this.getBounds(shape) @@ -70,7 +75,13 @@ export class DrawUtil extends TLDrawShapeUtil { return ( - + ) } @@ -78,30 +89,32 @@ export class DrawUtil extends TLDrawShapeUtil { const shouldFill = style.isFilled && points.length > 3 && - Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2 + Vec.dist(points[0], points[points.length - 1]) < strokeWidth * 2 if (shape.style.dash === DashStyle.Draw) { return ( - {shouldFill && ( + + {shouldFill && ( + + )} - )} - + ) } @@ -126,35 +139,37 @@ export class DrawUtil extends TLDrawShapeUtil { return ( - - + + + + ) }) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { const { points } = shape - const pathTLDrawSnapshot = React.useMemo(() => { - return getSolidStrokePathTLDrawSnapshot(shape) + const pathTDSnapshot = React.useMemo(() => { + return getSolidStrokePathTDSnapshot(shape) }, [points]) const bounds = this.getBounds(shape) @@ -165,13 +180,13 @@ export class DrawUtil extends TLDrawShapeUtil { return } - return + return }) transform = ( shape: T, bounds: TLBounds, - { initialShape, scaleX, scaleY }: TLDrawTransformInfo + { initialShape, scaleX, scaleY }: TransformInfo ): Partial => { const initialShapeBounds = Utils.getFromCache(this.boundsCache, initialShape, () => Utils.getBoundsFromPoints(initialShape.points) @@ -238,6 +253,32 @@ export class DrawUtil extends TLDrawShapeUtil { ) } + hitTestPoint = (shape: T, point: number[]) => { + const ptA = Vec.sub(point, shape.point) + return Utils.pointInPolyline(ptA, shape.points) + } + + hitTestLineSegment = (shape: T, A: number[], B: number[]): boolean => { + const { points, point } = shape + const ptA = Vec.sub(A, point) + const ptB = Vec.sub(B, point) + const bounds = this.getBounds(shape) + + if (points.length <= 2) { + return Vec.distanceToLineSegment(A, B, shape.point) < 4 + } + + if (intersectLineSegmentBounds(ptA, ptB, bounds)) { + for (let i = 1; i < points.length; i++) { + if (intersectLineSegmentLineSegment(points[i - 1], points[i], ptA, ptB).didIntersect) { + return true + } + } + } + + return false + } + hitTestBounds = (shape: T, bounds: TLBounds) => { // Test axis-aligned shape if (!shape.rotation) { diff --git a/packages/tldraw/src/state/shapes/DrawUtil/__snapshots__/DrawUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/DrawUtil/__snapshots__/DrawUtil.spec.tsx.snap index b70a3af8b..ee4bb2c55 100644 --- a/packages/tldraw/src/state/shapes/DrawUtil/__snapshots__/DrawUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/DrawUtil/__snapshots__/DrawUtil.spec.tsx.snap @@ -17,6 +17,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "draw", diff --git a/packages/tldraw/src/state/shapes/DrawUtil/drawHelpers.ts b/packages/tldraw/src/state/shapes/DrawUtil/drawHelpers.ts index 6e288731b..79304de56 100644 --- a/packages/tldraw/src/state/shapes/DrawUtil/drawHelpers.ts +++ b/packages/tldraw/src/state/shapes/DrawUtil/drawHelpers.ts @@ -1,7 +1,7 @@ import { Utils } from '@tldraw/core' import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand' import type { DrawShape } from '~types' -import { getShapeStyle } from '../shape-styles' +import { getShapeStyle } from '../shared/shape-styles' const simulatePressureSettings: StrokeOptions = { easing: (t) => Math.sin((t * Math.PI) / 2), @@ -43,7 +43,7 @@ export function getDrawStrokePoints(shape: DrawShape, options: StrokeOptions) { /** * Get path data for a stroke with the DashStyle.Draw dash style. */ -export function getDrawStrokePathTLDrawSnapshot(shape: DrawShape) { +export function getDrawStrokePathTDSnapshot(shape: DrawShape) { if (shape.points.length < 2) return '' const options = getFreehandOptions(shape) @@ -60,7 +60,7 @@ export function getDrawStrokePathTLDrawSnapshot(shape: DrawShape) { /** * Get SVG path data for a shape that has a DashStyle other than DashStyles.Draw. */ -export function getSolidStrokePathTLDrawSnapshot(shape: DrawShape) { +export function getSolidStrokePathTDSnapshot(shape: DrawShape) { const { points } = shape if (points.length < 2) return 'M 0 0 L 0 0' diff --git a/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx b/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx index 3af8b172a..99a64c63c 100644 --- a/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx +++ b/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx @@ -1,29 +1,22 @@ import * as React from 'react' import { Utils, SVGContainer, TLBounds } from '@tldraw/core' import { Vec } from '@tldraw/vec' -import { defaultStyle, getShapeStyle } from '../shape-styles' -import { - EllipseShape, - DashStyle, - TLDrawShapeType, - TLDrawShape, - TLDrawTransformInfo, - TLDrawMeta, -} from '~types' -import { BINDING_DISTANCE } from '~constants' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' +import { defaultStyle, getShapeStyle } from '~state/shapes/shared' +import { EllipseShape, DashStyle, TDShapeType, TDShape, TransformInfo, TDMeta } from '~types' +import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants' +import { TDShapeUtil } from '../TDShapeUtil' import { intersectEllipseBounds, intersectLineSegmentEllipse, intersectRayEllipse, } from '@tldraw/intersect' -import { getEllipseIndicatorPathTLDrawSnapshot, getEllipsePath } from './ellipseHelpers' +import { getEllipseIndicatorPathTDSnapshot, getEllipsePath } from './ellipseHelpers' type T = EllipseShape type E = SVGSVGElement -export class EllipseUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Ellipse as const +export class EllipseUtil extends TDShapeUtil { + type = TDShapeType.Ellipse as const canBind = true @@ -31,7 +24,7 @@ export class EllipseUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Ellipse, + type: TDShapeType.Ellipse, name: 'Ellipse', parentId: 'page', childIndex: 1, @@ -44,8 +37,8 @@ export class EllipseUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component( - ({ shape, isBinding, meta, events }, ref) => { + Component = TDShapeUtil.Component( + ({ shape, isGhost, isBinding, meta, events }, ref) => { const { radius: [radiusX, radiusY], style, @@ -53,7 +46,7 @@ export class EllipseUtil extends TLDrawShapeUtil { const styles = getShapeStyle(style, meta.isDarkMode) - const strokeWidth = +styles.strokeWidth + const strokeWidth = styles.strokeWidth const sw = 1 + strokeWidth * 1.618 @@ -75,7 +68,7 @@ export class EllipseUtil extends TLDrawShapeUtil { /> )} { pointerEvents="all" strokeLinecap="round" strokeLinejoin="round" + opacity={isGhost ? GHOSTED_OPACITY : 1} /> ) @@ -134,10 +128,34 @@ export class EllipseUtil extends TLDrawShapeUtil { } ) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { - return + Indicator = TDShapeUtil.Indicator(({ shape }) => { + return }) + hitTestPoint = (shape: T, point: number[]): boolean => { + return ( + Utils.pointInBounds(point, this.getRotatedBounds(shape)) && + Utils.pointInEllipse( + point, + this.getCenter(shape), + shape.radius[0], + shape.radius[1], + shape.rotation || 0 + ) + ) + } + + hitTestLineSegment = (shape: T, A: number[], B: number[]): boolean => { + return intersectLineSegmentEllipse( + A, + B, + this.getCenter(shape), + shape.radius[0], + shape.radius[1], + shape.rotation || 0 + ).didIntersect + } + getBounds = (shape: T) => { return Utils.getFromCache(this.boundsCache, shape, () => { return Utils.getRotatedEllipseBounds( @@ -183,7 +201,7 @@ export class EllipseUtil extends TLDrawShapeUtil { return Vec.add(shape.point, shape.radius) } - getBindingPoint = ( + getBindingPoint = ( shape: T, fromShape: K, point: number[], @@ -292,7 +310,7 @@ export class EllipseUtil extends TLDrawShapeUtil { transform = ( shape: T, bounds: TLBounds, - { scaleX, scaleY, initialShape }: TLDrawTransformInfo + { scaleX, scaleY, initialShape }: TransformInfo ): Partial => { const { rotation = 0 } = initialShape diff --git a/packages/tldraw/src/state/shapes/EllipseUtil/__snapshots__/EllipseUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/EllipseUtil/__snapshots__/EllipseUtil.spec.tsx.snap index 187933690..235edbdc5 100644 --- a/packages/tldraw/src/state/shapes/EllipseUtil/__snapshots__/EllipseUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/EllipseUtil/__snapshots__/EllipseUtil.spec.tsx.snap @@ -19,6 +19,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "ellipse", diff --git a/packages/tldraw/src/state/shapes/EllipseUtil/ellipseHelpers.ts b/packages/tldraw/src/state/shapes/EllipseUtil/ellipseHelpers.ts index 860ae40f7..348139d84 100644 --- a/packages/tldraw/src/state/shapes/EllipseUtil/ellipseHelpers.ts +++ b/packages/tldraw/src/state/shapes/EllipseUtil/ellipseHelpers.ts @@ -3,7 +3,7 @@ import Vec from '@tldraw/vec' import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand' import { EASINGS } from '~constants' import type { EllipseShape } from '~types' -import { getShapeStyle } from '../shape-styles' +import { getShapeStyle } from '../shared/shape-styles' export function getEllipseStrokePoints(shape: EllipseShape, boundsCenter: number[]) { const { @@ -78,7 +78,7 @@ export function getEllipsePath(shape: EllipseShape, boundsCenter: number[]) { ) } -export function getEllipseIndicatorPathTLDrawSnapshot(shape: EllipseShape, boundsCenter: number[]) { +export function getEllipseIndicatorPathTDSnapshot(shape: EllipseShape, boundsCenter: number[]) { return Utils.getSvgPathFromStroke( getEllipseStrokePoints(shape, boundsCenter).map((pt) => pt.point.slice(0, 2)), false diff --git a/packages/tldraw/src/state/shapes/GroupUtil/GroupUtil.tsx b/packages/tldraw/src/state/shapes/GroupUtil/GroupUtil.tsx index d6d52f74d..e5097d696 100644 --- a/packages/tldraw/src/state/shapes/GroupUtil/GroupUtil.tsx +++ b/packages/tldraw/src/state/shapes/GroupUtil/GroupUtil.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import { styled } from '~styles' import { Utils, SVGContainer } from '@tldraw/core' -import { defaultStyle } from '../shape-styles' -import { TLDrawShapeType, GroupShape, ColorStyle, TLDrawMeta } from '~types' -import { BINDING_DISTANCE } from '~constants' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' +import { defaultStyle } from '../shared/shape-styles' +import { TDShapeType, GroupShape, ColorStyle, TDMeta } from '~types' +import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants' +import { TDShapeUtil } from '../TDShapeUtil' import { getBoundsRectangle } from '../shared' type T = GroupShape type E = SVGSVGElement -export class GroupUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Group as const +export class GroupUtil extends TDShapeUtil { + type = TDShapeType.Group as const canBind = true @@ -19,7 +19,7 @@ export class GroupUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Group, + type: TDShapeType.Group, name: 'Group', parentId: 'page', childIndex: 1, @@ -33,8 +33,8 @@ export class GroupUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component( - ({ shape, isBinding, isHovered, isSelected, events }, ref) => { + Component = TDShapeUtil.Component( + ({ shape, isBinding, isGhost, isHovered, isSelected, events }, ref) => { const { id, size } = shape const sw = 2 @@ -63,28 +63,30 @@ export class GroupUtil extends TLDrawShapeUtil { height={size[1] + BINDING_DISTANCE * 2} /> )} - - - {paths} - + + + + {paths} + + ) } ) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { const { id, size } = shape const sw = 2 diff --git a/packages/tldraw/src/state/shapes/GroupUtil/__snapshots__/GroupUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/GroupUtil/__snapshots__/GroupUtil.spec.tsx.snap index 00e86c99a..5d7449fed 100644 --- a/packages/tldraw/src/state/shapes/GroupUtil/__snapshots__/GroupUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/GroupUtil/__snapshots__/GroupUtil.spec.tsx.snap @@ -20,6 +20,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "group", diff --git a/packages/tldraw/src/state/shapes/RectangleUtil/RectangleUtil.tsx b/packages/tldraw/src/state/shapes/RectangleUtil/RectangleUtil.tsx index fb32efec2..7550d6706 100644 --- a/packages/tldraw/src/state/shapes/RectangleUtil/RectangleUtil.tsx +++ b/packages/tldraw/src/state/shapes/RectangleUtil/RectangleUtil.tsx @@ -2,17 +2,22 @@ import * as React from 'react' import { Utils, SVGContainer } from '@tldraw/core' import { Vec } from '@tldraw/vec' import { getStroke, getStrokePoints } from 'perfect-freehand' -import { defaultStyle, getShapeStyle } from '../shape-styles' -import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types' -import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared' -import { BINDING_DISTANCE } from '~constants' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' +import { RectangleShape, DashStyle, TDShapeType, TDMeta } from '~types' +import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants' +import { TDShapeUtil } from '../TDShapeUtil' +import { + defaultStyle, + getShapeStyle, + getBoundsRectangle, + transformRectangle, + transformSingleRectangle, +} from '~state/shapes/shared' type T = RectangleShape type E = SVGSVGElement -export class RectangleUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Rectangle as const +export class RectangleUtil extends TDShapeUtil { + type = TDShapeType.Rectangle as const canBind = true @@ -20,7 +25,7 @@ export class RectangleUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, name: 'Rectangle', parentId: 'page', childIndex: 1, @@ -33,14 +38,16 @@ export class RectangleUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component( - ({ shape, isBinding, meta, events }, ref) => { + Component = TDShapeUtil.Component( + ({ shape, isBinding, isGhost, meta, events }, ref) => { const { id, size, style } = shape + const styles = getShapeStyle(style, meta.isDarkMode) - const strokeWidth = +styles.strokeWidth + + const { strokeWidth } = styles if (style.dash === DashStyle.Draw) { - const pathTLDrawSnapshot = getRectanglePath(shape) + const pathTDSnapshot = getRectanglePath(shape) return ( @@ -54,18 +61,19 @@ export class RectangleUtil extends TLDrawShapeUtil { /> )} ) @@ -133,19 +141,17 @@ export class RectangleUtil extends TLDrawShapeUtil { } ) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { const { style, size: [width, height], } = shape const styles = getShapeStyle(style, false) - const strokeWidth = +styles.strokeWidth - - const sw = strokeWidth + const sw = styles.strokeWidth if (style.dash === DashStyle.Draw) { - return + return } return ( @@ -262,7 +268,7 @@ function getRectanglePath(shape: RectangleShape) { return Utils.getSvgPathFromStroke(stroke) } -function getRectangleIndicatorPathTLDrawSnapshot(shape: RectangleShape) { +function getRectangleIndicatorPathTDSnapshot(shape: RectangleShape) { const { points, options } = getDrawStrokeInfo(shape) const strokePoints = getStrokePoints(points, options) diff --git a/packages/tldraw/src/state/shapes/RectangleUtil/__snapshots__/RectangleUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/RectangleUtil/__snapshots__/RectangleUtil.spec.tsx.snap index 5cd1eac74..0dfbb638c 100644 --- a/packages/tldraw/src/state/shapes/RectangleUtil/__snapshots__/RectangleUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/RectangleUtil/__snapshots__/RectangleUtil.spec.tsx.snap @@ -19,6 +19,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "type": "rectangle", diff --git a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx index a7a4602a4..162dbf31f 100644 --- a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx +++ b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx @@ -1,29 +1,33 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import { Utils, HTMLContainer, TLBounds } from '@tldraw/core' -import { defaultStyle } from '../shape-styles' -import { StickyShape, TLDrawMeta, TLDrawShapeType, TLDrawTransformInfo } from '~types' +import { defaultStyle } from '../shared/shape-styles' +import { StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types' import { getBoundsRectangle, TextAreaUtils } from '../shared' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' -import { getStickyFontStyle, getStickyShapeStyle } from '../shape-styles' +import { TDShapeUtil } from '../TDShapeUtil' +import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles' import { styled } from '~styles' import Vec from '@tldraw/vec' +import { GHOSTED_OPACITY } from '~constants' +import { TLDR } from '~state/TLDR' type T = StickyShape type E = HTMLDivElement -export class StickyUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Sticky as const +export class StickyUtil extends TDShapeUtil { + type = TDShapeType.Sticky as const canBind = true canEdit = true + hideResizeHandles = true + getShape = (props: Partial): T => { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Sticky, + type: TDShapeType.Sticky, name: 'Sticky', parentId: 'page', childIndex: 1, @@ -37,8 +41,8 @@ export class StickyUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component( - ({ shape, meta, events, isEditing, onShapeBlur, onShapeChange }, ref) => { + Component = TDShapeUtil.Component( + ({ shape, meta, events, isGhost, isEditing, onShapeBlur, onShapeChange }, ref) => { const font = getStickyFontStyle(shape.style) const { color, fill } = getStickyShapeStyle(shape.style, meta.isDarkMode) @@ -60,7 +64,7 @@ export class StickyUtil extends TLDrawShapeUtil { onShapeChange?.({ id: shape.id, type: shape.type, - text: normalizeText(e.currentTarget.value), + text: TLDR.normalizeText(e.currentTarget.value), }) }, [onShapeChange] @@ -85,7 +89,7 @@ export class StickyUtil extends TLDrawShapeUtil { TextAreaUtils.indent(e.currentTarget) } - onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) + onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) }) } }, [shape, onShapeChange] @@ -158,6 +162,7 @@ export class StickyUtil extends TLDrawShapeUtil { @@ -187,7 +192,7 @@ export class StickyUtil extends TLDrawShapeUtil { } ) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { const { size: [width, height], } = shape @@ -208,7 +213,7 @@ export class StickyUtil extends TLDrawShapeUtil { transform = ( shape: T, bounds: TLBounds, - { scaleX, scaleY, transformOrigin }: TLDrawTransformInfo + { scaleX, scaleY, transformOrigin }: TransformInfo ): Partial => { const point = Vec.round([ bounds.minX + @@ -235,13 +240,6 @@ export class StickyUtil extends TLDrawShapeUtil { const PADDING = 16 const MIN_CONTAINER_HEIGHT = 200 -const fixNewLines = /\r?\n|\r/g -const fixSpaces = / /g - -function normalizeText(text: string) { - return text.replace(fixNewLines, '\n').replace(fixSpaces, '\u00a0') -} - const StyledStickyContainer = styled('div', { pointerEvents: 'all', position: 'relative', @@ -253,6 +251,10 @@ const StyledStickyContainer = styled('div', { borderRadius: '3px', perspective: '800px', variants: { + isGhost: { + false: { opacity: 1 }, + true: { transition: 'opacity .2s', opacity: GHOSTED_OPACITY }, + }, isDarkMode: { true: { boxShadow: diff --git a/packages/tldraw/src/state/shapes/TLDrawShapeUtil/TLDrawShapeUtil.tsx b/packages/tldraw/src/state/shapes/TDShapeUtil.tsx similarity index 76% rename from packages/tldraw/src/state/shapes/TLDrawShapeUtil/TLDrawShapeUtil.tsx rename to packages/tldraw/src/state/shapes/TDShapeUtil.tsx index 0fbf8e317..15f928f93 100644 --- a/packages/tldraw/src/state/shapes/TLDrawShapeUtil/TLDrawShapeUtil.tsx +++ b/packages/tldraw/src/state/shapes/TDShapeUtil.tsx @@ -2,15 +2,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Utils, TLShapeUtil } from '@tldraw/core' import type { TLPointerInfo, TLBounds } from '@tldraw/core' -import { intersectRayBounds } from '@tldraw/intersect' +import { + intersectLineSegmentBounds, + intersectLineSegmentPolyline, + intersectRayBounds, +} from '@tldraw/intersect' import { Vec } from '@tldraw/vec' -import type { TLDrawBinding, TLDrawMeta, TLDrawShape, TLDrawTransformInfo } from '~types' +import type { TDBinding, TDMeta, TDShape, TransformInfo } from '~types' import * as React from 'react' -export abstract class TLDrawShapeUtil< - T extends TLDrawShape, - E extends Element = any -> extends TLShapeUtil { +export abstract class TDShapeUtil extends TLShapeUtil< + T, + E, + TDMeta +> { abstract type: T['type'] canBind = false @@ -21,8 +26,24 @@ export abstract class TLDrawShapeUtil< isAspectRatioLocked = false + hideResizeHandles = false + abstract getShape: (props: Partial) => T + hitTestPoint = (shape: T, point: number[]): boolean => { + return Utils.pointInBounds(point, this.getRotatedBounds(shape)) + } + + hitTestLineSegment = (shape: T, A: number[], B: number[]): boolean => { + const box = Utils.getBoundsFromPoints([A, B]) + const bounds = this.getBounds(shape) + + return Utils.boundsContain(bounds, box) || shape.rotation + ? intersectLineSegmentPolyline(A, B, Utils.getRotatedCorners(this.getBounds(shape))) + .didIntersect + : intersectLineSegmentBounds(A, B, this.getBounds(shape)).length > 0 + } + create = (props: { id: string } & Partial) => { this.refMap.set(props.id, React.createRef()) return this.getShape(props) @@ -32,7 +53,7 @@ export abstract class TLDrawShapeUtil< return Utils.getBoundsCenter(this.getBounds(shape)) } - getBindingPoint = ( + getBindingPoint = ( shape: T, fromShape: K, point: number[], @@ -117,26 +138,22 @@ export abstract class TLDrawShapeUtil< return props } - transform = (shape: T, bounds: TLBounds, info: TLDrawTransformInfo): Partial => { + transform = (shape: T, bounds: TLBounds, info: TransformInfo): Partial => { return { ...shape, point: [bounds.minX, bounds.minY] } } - transformSingle = ( - shape: T, - bounds: TLBounds, - info: TLDrawTransformInfo - ): Partial | void => { + transformSingle = (shape: T, bounds: TLBounds, info: TransformInfo): Partial | void => { return this.transform(shape, bounds, info) } - updateChildren?: (shape: T, children: K[]) => Partial[] | void + updateChildren?: (shape: T, children: K[]) => Partial[] | void - onChildrenChange?: (shape: T, children: TLDrawShape[]) => Partial | void + onChildrenChange?: (shape: T, children: TDShape[]) => Partial | void onBindingChange?: ( shape: T, - binding: TLDrawBinding, - target: TLDrawShape, + binding: TDBinding, + target: TDShape, targetBounds: TLBounds, center: number[] ) => Partial | void diff --git a/packages/tldraw/src/state/shapes/TLDrawShapeUtil/index.ts b/packages/tldraw/src/state/shapes/TLDrawShapeUtil/index.ts deleted file mode 100644 index d6b602fcf..000000000 --- a/packages/tldraw/src/state/shapes/TLDrawShapeUtil/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './TLDrawShapeUtil' diff --git a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx index 312622cc9..5f998e19f 100644 --- a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx +++ b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx @@ -1,19 +1,20 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import { Utils, HTMLContainer, TLBounds } from '@tldraw/core' -import { defaultStyle, getShapeStyle, getFontStyle } from '../shape-styles' -import { TextShape, TLDrawMeta, TLDrawShapeType, TLDrawTransformInfo } from '~types' +import { defaultStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles' +import { TextShape, TDMeta, TDShapeType, TransformInfo } from '~types' import { TextAreaUtils } from '../shared' -import { BINDING_DISTANCE } from '~constants' -import { TLDrawShapeUtil } from '../TLDrawShapeUtil' +import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants' +import { TDShapeUtil } from '../TDShapeUtil' import { styled } from '~styles' import Vec from '@tldraw/vec' +import { TLDR } from '~state/TLDR' type T = TextShape type E = HTMLDivElement -export class TextUtil extends TLDrawShapeUtil { - type = TLDrawShapeType.Text as const +export class TextUtil extends TDShapeUtil { + type = TDShapeType.Text as const isAspectRatioLocked = true @@ -25,7 +26,7 @@ export class TextUtil extends TLDrawShapeUtil { return Utils.deepMerge( { id: 'id', - type: TLDrawShapeType.Text, + type: TDShapeType.Text, name: 'Text', parentId: 'page', childIndex: 1, @@ -38,8 +39,8 @@ export class TextUtil extends TLDrawShapeUtil { ) } - Component = TLDrawShapeUtil.Component( - ({ shape, isBinding, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => { + Component = TDShapeUtil.Component( + ({ shape, isBinding, isGhost, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => { const rInput = React.useRef(null) const { text, style } = shape const styles = getShapeStyle(style, meta.isDarkMode) @@ -49,7 +50,7 @@ export class TextUtil extends TLDrawShapeUtil { const handleChange = React.useCallback( (e: React.ChangeEvent) => { - onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) + onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) }) }, [shape] ) @@ -71,7 +72,7 @@ export class TextUtil extends TLDrawShapeUtil { TextAreaUtils.indent(e.currentTarget) } - onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) + onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) }) } }, [shape, onShapeChange] @@ -118,7 +119,7 @@ export class TextUtil extends TLDrawShapeUtil { return ( - + { ) : ( text )} + ​ @@ -174,7 +176,7 @@ export class TextUtil extends TLDrawShapeUtil { } ) - Indicator = TLDrawShapeUtil.Indicator(({ shape }) => { + Indicator = TDShapeUtil.Indicator(({ shape }) => { const { width, height } = this.getBounds(shape) return }) @@ -215,7 +217,7 @@ export class TextUtil extends TLDrawShapeUtil { transform = ( shape: T, bounds: TLBounds, - { initialShape, scaleX, scaleY }: TLDrawTransformInfo + { initialShape, scaleX, scaleY }: TransformInfo ): Partial => { const { rotation = 0, @@ -238,7 +240,7 @@ export class TextUtil extends TLDrawShapeUtil { transformSingle = ( shape: T, bounds: TLBounds, - { initialShape, scaleX, scaleY }: TLDrawTransformInfo + { initialShape, scaleX, scaleY }: TransformInfo ): Partial | void => { const { style: { scale = 1 }, @@ -280,13 +282,6 @@ export class TextUtil extends TLDrawShapeUtil { const LETTER_SPACING = -1.5 -const fixNewLines = /\r?\n|\r/g -const fixSpaces = / /g - -function normalizeText(text: string) { - return text.replace(fixNewLines, '\n').replace(fixSpaces, '\u00a0') -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any let melm: any @@ -329,6 +324,10 @@ const Wrapper = styled('div', { width: '100%', height: '100%', variants: { + isGhost: { + false: { opacity: 1 }, + true: { transition: 'opacity .2s', opacity: GHOSTED_OPACITY }, + }, isEditing: { false: { pointerEvents: 'all', @@ -342,6 +341,11 @@ const Wrapper = styled('div', { }, }) +const commonTextWrapping = { + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', +} + const InnerWrapper = styled('div', { position: 'absolute', top: 'var(--tl-padding)', @@ -370,6 +374,7 @@ const InnerWrapper = styled('div', { WebkitUserSelect: 'text', }, }, + ...commonTextWrapping, }) const TextArea = styled('textarea', { @@ -381,7 +386,6 @@ const TextArea = styled('textarea', { height: '100%', border: 'none', padding: '4px', - whiteSpace: 'pre', resize: 'none', minHeight: 'inherit', minWidth: 'inherit', @@ -396,4 +400,5 @@ const TextArea = styled('textarea', { background: '$boundsBg', userSelect: 'text', WebkitUserSelect: 'text', + ...commonTextWrapping, }) diff --git a/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap index a4b485f15..d79d3a67f 100644 --- a/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap @@ -15,6 +15,7 @@ Object { "color": "black", "dash": "draw", "isFilled": false, + "scale": 1, "size": "small", }, "text": " ", diff --git a/packages/tldraw/src/state/shapes/index.ts b/packages/tldraw/src/state/shapes/index.ts index 7d2dbb4ce..7d360d43c 100644 --- a/packages/tldraw/src/state/shapes/index.ts +++ b/packages/tldraw/src/state/shapes/index.ts @@ -1,4 +1,4 @@ -import type { TLDrawShapeUtil } from './TLDrawShapeUtil' +import type { TDShapeUtil } from './TDShapeUtil' import { RectangleUtil } from './RectangleUtil' import { EllipseUtil } from './EllipseUtil' import { ArrowUtil } from './ArrowUtil' @@ -6,7 +6,7 @@ import { GroupUtil } from './GroupUtil' import { StickyUtil } from './StickyUtil' import { TextUtil } from './TextUtil' import { DrawUtil } from './DrawUtil' -import { TLDrawShape, TLDrawShapeType } from '~types' +import { TDShape, TDShapeType } from '~types' export const Rectangle = new RectangleUtil() export const Ellipse = new EllipseUtil() @@ -17,16 +17,16 @@ export const Group = new GroupUtil() export const Sticky = new StickyUtil() export const shapeUtils = { - [TLDrawShapeType.Rectangle]: Rectangle, - [TLDrawShapeType.Ellipse]: Ellipse, - [TLDrawShapeType.Draw]: Draw, - [TLDrawShapeType.Arrow]: Arrow, - [TLDrawShapeType.Text]: Text, - [TLDrawShapeType.Group]: Group, - [TLDrawShapeType.Sticky]: Sticky, + [TDShapeType.Rectangle]: Rectangle, + [TDShapeType.Ellipse]: Ellipse, + [TDShapeType.Draw]: Draw, + [TDShapeType.Arrow]: Arrow, + [TDShapeType.Text]: Text, + [TDShapeType.Group]: Group, + [TDShapeType.Sticky]: Sticky, } -export const getShapeUtils = (shape: T | T['type']) => { - if (typeof shape === 'string') return shapeUtils[shape] as unknown as TLDrawShapeUtil - return shapeUtils[shape.type] as unknown as TLDrawShapeUtil +export const getShapeUtil = (shape: T | T['type']) => { + if (typeof shape === 'string') return shapeUtils[shape] as unknown as TDShapeUtil + return shapeUtils[shape.type] as unknown as TDShapeUtil } diff --git a/packages/tldraw/src/state/shapes/shared/TextAreaUtils.ts b/packages/tldraw/src/state/shapes/shared/TextAreaUtils.ts index 41e61f657..e7de7ba96 100644 --- a/packages/tldraw/src/state/shapes/shared/TextAreaUtils.ts +++ b/packages/tldraw/src/state/shapes/shared/TextAreaUtils.ts @@ -113,10 +113,10 @@ export class TextAreaUtils { static indent(element: HTMLTextAreaElement): void { const { selectionStart, selectionEnd, value } = element - const selectedText = value.slice(selectionStart, selectionEnd) + const selectedContrast = value.slice(selectionStart, selectionEnd) // The first line should be indented, even if it starts with `\n` // The last line should only be indented if includes any character after `\n` - const lineBreakCount = /\n/g.exec(selectedText)?.length + const lineBreakCount = /\n/g.exec(selectedContrast)?.length if (lineBreakCount && lineBreakCount > 0) { // Select full first line to replace everything at once diff --git a/packages/tldraw/src/state/shapes/shared/index.ts b/packages/tldraw/src/state/shapes/shared/index.ts index 6f75660ff..ca2187a82 100644 --- a/packages/tldraw/src/state/shapes/shared/index.ts +++ b/packages/tldraw/src/state/shapes/shared/index.ts @@ -2,3 +2,4 @@ export * from './getBoundsRectangle' export * from './transformRectangle' export * from './transformSingleRectangle' export * from './TextAreaUtils' +export * from './shape-styles' diff --git a/packages/tldraw/src/state/shapes/shape-styles.ts b/packages/tldraw/src/state/shapes/shared/shape-styles.ts similarity index 85% rename from packages/tldraw/src/state/shapes/shape-styles.ts rename to packages/tldraw/src/state/shapes/shared/shape-styles.ts index 76ac71de9..14730daa9 100644 --- a/packages/tldraw/src/state/shapes/shape-styles.ts +++ b/packages/tldraw/src/state/shapes/shared/shape-styles.ts @@ -6,10 +6,10 @@ const canvasLight = '#fafafa' const canvasDark = '#343d45' const colors = { - [ColorStyle.Black]: '#212528', [ColorStyle.White]: '#f0f1f3', [ColorStyle.LightGray]: '#c6cbd1', [ColorStyle.Gray]: '#788492', + [ColorStyle.Black]: '#1d1d1d', [ColorStyle.Green]: '#36b24d', [ColorStyle.Cyan]: '#0e98ad', [ColorStyle.Blue]: '#1c7ed6', @@ -35,19 +35,22 @@ export const stickyFills: Record> = { Utils.lerpColor(Utils.lerpColor(v, '#999999', 0.3), canvasDark, 0.4), ]) ) as Record), - [ColorStyle.White]: '#bbbbbb', - [ColorStyle.Black]: '#1d1d1d', + [ColorStyle.White]: '#1d1d1d', + [ColorStyle.Black]: '#bbbbbb', }, } export const strokes: Record> = { - light: colors, + light: { + ...colors, + [ColorStyle.White]: '#1d1d1d', + }, dark: { ...(Object.fromEntries( Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasDark, 0.1)]) ) as Record), - [ColorStyle.White]: '#ffffff', - [ColorStyle.Black]: '#000', + [ColorStyle.White]: '#cecece', + [ColorStyle.Black]: '#cecece', }, } @@ -56,12 +59,16 @@ export const fills: Record> = { ...(Object.fromEntries( Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasLight, 0.82)]) ) as Record), - [ColorStyle.White]: '#ffffff', - [ColorStyle.Black]: '#ffffff', + [ColorStyle.White]: '#fefefe', + [ColorStyle.Black]: '#4d4d4d', + }, + dark: { + ...(Object.fromEntries( + Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasDark, 0.618)]) + ) as Record), + [ColorStyle.White]: 'rgb(30,33,37)', + [ColorStyle.Black]: '#1e1e1f', }, - dark: Object.fromEntries( - Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasDark, 0.618)]) - ) as Record, } const strokeWidths = { @@ -125,7 +132,7 @@ export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) { export function getShapeStyle( style: ShapeStyles, - isDarkMode = false + isDarkMode?: boolean ): { stroke: string fill: string @@ -149,4 +156,5 @@ export const defaultStyle: ShapeStyles = { size: SizeStyle.Small, isFilled: false, dash: DashStyle.Draw, + scale: 1, } diff --git a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.spec.ts b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.spec.ts index 7a4bfd9d0..804e2a605 100644 --- a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.spec.ts +++ b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { ArrowTool } from '.' describe('ArrowTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new ArrowTool(state) + const app = new TldrawApp() + new ArrowTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts index 1e0246b4a..db607c1a1 100644 --- a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts +++ b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts @@ -1,20 +1,18 @@ -import Vec from '@tldraw/vec' import { Utils, TLPointerEventHandler } from '@tldraw/core' import { Arrow } from '~state/shapes' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class ArrowTool extends BaseTool { - type = TLDrawShapeType.Arrow + type = TDShapeType.Arrow as const /* ----------------- Event Handlers ----------------- */ - onPointerDown: TLPointerEventHandler = (info) => { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - + onPointerDown: TLPointerEventHandler = () => { const { + currentPoint, appState: { currentPageId, currentStyle }, - } = this.state + } = this.app const childIndex = this.getNextChildIndex() @@ -24,13 +22,13 @@ export class ArrowTool extends BaseTool { id, parentId: currentPageId, childIndex, - point: pagePoint, + point: currentPoint, style: { ...currentStyle }, }) - this.state.patchCreate([newShape]) + this.app.patchCreate([newShape]) - this.state.startSession(SessionType.Arrow, pagePoint, 'end', true) + this.app.startSession(SessionType.Arrow, newShape.id, 'end', true) this.setStatus(Status.Creating) } diff --git a/packages/tldraw/src/state/tools/BaseTool.ts b/packages/tldraw/src/state/tools/BaseTool.ts new file mode 100644 index 000000000..f07e548d9 --- /dev/null +++ b/packages/tldraw/src/state/tools/BaseTool.ts @@ -0,0 +1,128 @@ +import { + TLKeyboardEventHandler, + TLPinchEventHandler, + TLPointerEventHandler, + Utils, +} from '@tldraw/core' +import type { TldrawApp } from '../internal' +import { TDEventHandler, TDToolType } from '~types' + +export enum Status { + Idle = 'idle', + Creating = 'creating', + Pinching = 'pinching', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export abstract class BaseTool extends TDEventHandler { + type: TDToolType = 'select' as const + + previous?: TDToolType + + status: Status | T = Status.Idle + + constructor(public app: TldrawApp) { + super() + } + + protected readonly setStatus = (status: Status | T) => { + this.status = status as Status | T + this.app.setStatus(this.status as string) + } + + onEnter = () => { + this.setStatus(Status.Idle) + } + + onExit = () => { + this.setStatus(Status.Idle) + } + + onCancel = () => { + if (this.status === Status.Idle) { + this.app.selectTool('select') + } else { + this.setStatus(Status.Idle) + } + + this.app.cancelSession() + } + + getNextChildIndex = () => { + const { + shapes, + appState: { currentPageId }, + } = this.app + + return shapes.length === 0 + ? 1 + : shapes + .filter((shape) => shape.parentId === currentPageId) + .sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1 + } + + /* --------------------- Camera --------------------- */ + + onPinchStart: TLPinchEventHandler = () => { + this.app.cancelSession() + this.setStatus(Status.Pinching) + } + + onPinchEnd: TLPinchEventHandler = () => { + if (Utils.isMobileSafari()) { + this.app.undoSelect() + } + this.setStatus(Status.Idle) + } + + onPinch: TLPinchEventHandler = (info, e) => { + if (this.status !== 'pinching') return + this.app.pinchZoom(info.point, info.delta, info.delta[2]) + this.onPointerMove?.(info, e as unknown as React.PointerEvent) + } + + /* ---------------------- Keys ---------------------- */ + + onKeyDown: TLKeyboardEventHandler = (key) => { + if (key === 'Escape') { + this.onCancel() + return + } + + /* noop */ + if (key === 'Meta' || key === 'Control' || key === 'Alt') { + this.app.updateSession() + return + } + } + + onKeyUp: TLKeyboardEventHandler = (key) => { + /* noop */ + if (key === 'Meta' || key === 'Control' || key === 'Alt') { + this.app.updateSession() + return + } + } + + /* --------------------- Pointer -------------------- */ + + onPointerMove: TLPointerEventHandler = () => { + if (this.status === Status.Creating) { + this.app.updateSession() + } + } + + onPointerUp: TLPointerEventHandler = () => { + if (this.status === Status.Creating) { + this.app.completeSession() + + const { isToolLocked } = this.app.appState + + if (!isToolLocked) { + this.app.selectTool('select') + } + } + + this.setStatus(Status.Idle) + } +} diff --git a/packages/tldraw/src/state/tools/BaseTool/BaseTool.ts b/packages/tldraw/src/state/tools/BaseTool/BaseTool.ts deleted file mode 100644 index 52b56d8bc..000000000 --- a/packages/tldraw/src/state/tools/BaseTool/BaseTool.ts +++ /dev/null @@ -1,193 +0,0 @@ -import Vec from '@tldraw/vec' -import { - TLBoundsEventHandler, - TLBoundsHandleEventHandler, - TLCanvasEventHandler, - TLKeyboardEventHandler, - TLPinchEventHandler, - TLPointerEventHandler, - TLShapeBlurHandler, - TLShapeCloneHandler, - TLWheelEventHandler, - Utils, -} from '@tldraw/core' -import type { TLDrawState } from '~state' - -export enum Status { - Idle = 'idle', - Creating = 'creating', - Pinching = 'pinching', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export abstract class BaseTool { - state: TLDrawState - - status: Status | T = Status.Idle - - constructor(state: TLDrawState) { - this.state = state - } - - protected readonly setStatus = (status: Status | T) => { - this.status = status as Status | T - this.state.setStatus(this.status as string) - } - - onEnter = () => { - this.setStatus(Status.Idle) - } - - onExit = () => { - this.setStatus(Status.Idle) - } - - onCancel = () => { - if (this.status === Status.Idle) { - this.state.selectTool('select') - } else { - this.setStatus(Status.Idle) - } - - this.state.cancelSession() - } - - getNextChildIndex = () => { - const { - shapes, - appState: { currentPageId }, - } = this.state - - return shapes.length === 0 - ? 1 - : shapes - .filter((shape) => shape.parentId === currentPageId) - .sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1 - } - - /* --------------------- Camera --------------------- */ - - onPinchStart: TLPinchEventHandler = () => { - this.state.cancelSession() - this.setStatus(Status.Pinching) - } - - onPinchEnd: TLPinchEventHandler = () => { - if (Utils.isMobileSafari()) { - this.state.undoSelect() - } - this.setStatus(Status.Idle) - } - - onPinch: TLPinchEventHandler = (info, e) => { - if (this.status !== 'pinching') return - this.state.pinchZoom(info.point, info.delta, info.delta[2]) - this.onPointerMove?.(info, e as unknown as React.PointerEvent) - } - - /* ---------------------- Keys ---------------------- */ - - onKeyDown: TLKeyboardEventHandler = (key, info) => { - if (key === 'Escape') { - this.onCancel() - return - } - - /* noop */ - if (key === 'Meta' || key === 'Control' || key === 'Alt') { - this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) - return - } - } - - onKeyUp: TLKeyboardEventHandler = (key, info) => { - /* noop */ - if (key === 'Meta' || key === 'Control' || key === 'Alt') { - this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) - return - } - } - - /* --------------------- Pointer -------------------- */ - - onPointerMove: TLPointerEventHandler = (info) => { - if (this.status === Status.Creating) { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey) - } - } - - onPointerUp: TLPointerEventHandler = () => { - if (this.status === Status.Creating) { - this.state.completeSession() - } - - this.setStatus(Status.Idle) - } - - /* --------------------- Others --------------------- */ - - // Camera Events - onPan?: TLWheelEventHandler - onZoom?: TLWheelEventHandler - - // Pointer Events - onPointerDown?: TLPointerEventHandler - - // Canvas (background) - onPointCanvas?: TLCanvasEventHandler - onDoubleClickCanvas?: TLCanvasEventHandler - onRightPointCanvas?: TLCanvasEventHandler - onDragCanvas?: TLCanvasEventHandler - onReleaseCanvas?: TLCanvasEventHandler - - // Shape - onPointShape?: TLPointerEventHandler - onDoubleClickShape?: TLPointerEventHandler - onRightPointShape?: TLPointerEventHandler - onDragShape?: TLPointerEventHandler - onHoverShape?: TLPointerEventHandler - onUnhoverShape?: TLPointerEventHandler - onReleaseShape?: TLPointerEventHandler - - // Bounds (bounding box background) - onPointBounds?: TLBoundsEventHandler - onDoubleClickBounds?: TLBoundsEventHandler - onRightPointBounds?: TLBoundsEventHandler - onDragBounds?: TLBoundsEventHandler - onHoverBounds?: TLBoundsEventHandler - onUnhoverBounds?: TLBoundsEventHandler - onReleaseBounds?: TLBoundsEventHandler - - // Bounds handles (corners, edges) - onPointBoundsHandle?: TLBoundsHandleEventHandler - onDoubleClickBoundsHandle?: TLBoundsHandleEventHandler - onRightPointBoundsHandle?: TLBoundsHandleEventHandler - onDragBoundsHandle?: TLBoundsHandleEventHandler - onHoverBoundsHandle?: TLBoundsHandleEventHandler - onUnhoverBoundsHandle?: TLBoundsHandleEventHandler - onReleaseBoundsHandle?: TLBoundsHandleEventHandler - - // Handles (ie the handles of a selected arrow) - onPointHandle?: TLPointerEventHandler - onDoubleClickHandle?: TLPointerEventHandler - onRightPointHandle?: TLPointerEventHandler - onDragHandle?: TLPointerEventHandler - onHoverHandle?: TLPointerEventHandler - onUnhoverHandle?: TLPointerEventHandler - onReleaseHandle?: TLPointerEventHandler - - // Misc - onShapeBlur?: TLShapeBlurHandler - onShapeClone?: TLShapeCloneHandler -} diff --git a/packages/tldraw/src/state/tools/BaseTool/index.ts b/packages/tldraw/src/state/tools/BaseTool/index.ts deleted file mode 100644 index a3c12c21e..000000000 --- a/packages/tldraw/src/state/tools/BaseTool/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BaseTool' diff --git a/packages/tldraw/src/state/tools/DrawTool/DrawTool.spec.ts b/packages/tldraw/src/state/tools/DrawTool/DrawTool.spec.ts index df465e521..1ef3c9846 100644 --- a/packages/tldraw/src/state/tools/DrawTool/DrawTool.spec.ts +++ b/packages/tldraw/src/state/tools/DrawTool/DrawTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { DrawTool } from '.' describe('DrawTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new DrawTool(state) + const app = new TldrawApp() + new DrawTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/DrawTool/DrawTool.ts b/packages/tldraw/src/state/tools/DrawTool/DrawTool.ts index 209574bd0..eaf67fabb 100644 --- a/packages/tldraw/src/state/tools/DrawTool/DrawTool.ts +++ b/packages/tldraw/src/state/tools/DrawTool/DrawTool.ts @@ -1,28 +1,20 @@ -import Vec from '@tldraw/vec' import { Utils, TLPointerEventHandler } from '@tldraw/core' import { Draw } from '~state/shapes' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class DrawTool extends BaseTool { - type = TLDrawShapeType.Draw + type = TDShapeType.Draw as const /* ----------------- Event Handlers ----------------- */ onPointerDown: TLPointerEventHandler = (info) => { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - const { - shapes, + currentPoint, appState: { currentPageId, currentStyle }, - } = this.state + } = this.app - const childIndex = - shapes.length === 0 - ? 1 - : shapes - .filter((shape) => shape.parentId === currentPageId) - .sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1 + const childIndex = this.getNextChildIndex() const id = Utils.uniqueId() @@ -30,32 +22,26 @@ export class DrawTool extends BaseTool { id, parentId: currentPageId, childIndex, - point: [...pagePoint, info.pressure || 0.5], + point: [...currentPoint, info.pressure || 0.5], style: { ...currentStyle }, }) - this.state.patchCreate([newShape]) + this.app.patchCreate([newShape]) - this.state.startSession(SessionType.Draw, pagePoint, id) + this.app.startSession(SessionType.Draw, id) this.setStatus(Status.Creating) } - onPointerMove: TLPointerEventHandler = (info) => { + onPointerMove: TLPointerEventHandler = () => { if (this.status === Status.Creating) { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - this.state.updateSession( - [...pagePoint, info.pressure || 0.5], - info.shiftKey, - info.altKey, - info.metaKey - ) + this.app.updateSession() } } onPointerUp: TLPointerEventHandler = () => { if (this.status === Status.Creating) { - this.state.completeSession() + this.app.completeSession() } this.setStatus(Status.Idle) diff --git a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.spec.ts b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.spec.ts index d9fc38f8e..a6a5df0d6 100644 --- a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.spec.ts +++ b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { EllipseTool } from '.' describe('EllipseTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new EllipseTool(state) + const app = new TldrawApp() + new EllipseTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts index a127c128c..16a7eebc4 100644 --- a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts +++ b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts @@ -1,20 +1,18 @@ -import Vec from '@tldraw/vec' import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core' import { Ellipse } from '~state/shapes' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class EllipseTool extends BaseTool { - type = TLDrawShapeType.Ellipse + type = TDShapeType.Ellipse as const /* ----------------- Event Handlers ----------------- */ - onPointerDown: TLPointerEventHandler = (info) => { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - + onPointerDown: TLPointerEventHandler = () => { const { + currentPoint, appState: { currentPageId, currentStyle }, - } = this.state + } = this.app const childIndex = this.getNextChildIndex() @@ -24,15 +22,15 @@ export class EllipseTool extends BaseTool { id, parentId: currentPageId, childIndex, - point: pagePoint, + point: currentPoint, style: { ...currentStyle }, }) - this.state.patchCreate([newShape]) + this.app.patchCreate([newShape]) - this.state.startSession( + this.app.startSession( SessionType.TransformSingle, - pagePoint, + newShape.id, TLBoundsCorner.BottomRight, true ) diff --git a/packages/tldraw/src/state/tools/EraseTool/EraseTool.spec.ts b/packages/tldraw/src/state/tools/EraseTool/EraseTool.spec.ts new file mode 100644 index 000000000..756119dc1 --- /dev/null +++ b/packages/tldraw/src/state/tools/EraseTool/EraseTool.spec.ts @@ -0,0 +1,11 @@ +import { TldrawApp } from '~state' +import { EraseTool } from './EraseTool' + +describe('EraseTool', () => { + it('creates tool', () => { + const app = new TldrawApp() + new EraseTool(app) + }) + + it.todo('restores previous tool after erasing') +}) diff --git a/packages/tldraw/src/state/tools/EraseTool/EraseTool.ts b/packages/tldraw/src/state/tools/EraseTool/EraseTool.ts new file mode 100644 index 000000000..54e6cbe7b --- /dev/null +++ b/packages/tldraw/src/state/tools/EraseTool/EraseTool.ts @@ -0,0 +1,81 @@ +import Vec from '@tldraw/vec' +import type { TLPointerEventHandler } from '@tldraw/core' +import { SessionType } from '~types' +import { BaseTool } from '../BaseTool' +import { DEAD_ZONE } from '~constants' + +enum Status { + Idle = 'idle', + Pointing = 'pointing', + Erasing = 'erasing', +} + +export class EraseTool extends BaseTool { + type = 'erase' as const + + status: Status = Status.Idle + + /* ----------------- Event Handlers ----------------- */ + + onPointerDown: TLPointerEventHandler = () => { + this.setStatus(Status.Pointing) + } + + onPointerMove: TLPointerEventHandler = (info) => { + switch (this.status) { + case Status.Pointing: { + if (Vec.dist(info.origin, info.point) > DEAD_ZONE) { + this.app.startSession(SessionType.Erase) + this.app.updateSession() + this.setStatus(Status.Erasing) + } + break + } + case Status.Erasing: { + this.app.updateSession() + } + } + } + + onPointerUp: TLPointerEventHandler = () => { + switch (this.status) { + case Status.Pointing: { + const shapeIdsAtPoint = this.app.shapes + .filter((shape) => !shape.isLocked) + .filter((shape) => + this.app.getShapeUtil(shape).hitTestPoint(shape, this.app.currentPoint) + ) + .flatMap((shape) => (shape.children ? [shape.id, ...shape.children] : shape.id)) + + this.app.delete(shapeIdsAtPoint) + + break + } + case Status.Erasing: { + this.app.completeSession() + + if (this.previous) { + this.app.selectTool(this.previous) + } else { + this.app.selectTool('select') + } + } + } + + this.setStatus(Status.Idle) + } + + onCancel = () => { + if (this.status === Status.Idle) { + if (this.previous) { + this.app.selectTool(this.previous) + } else { + this.app.selectTool('select') + } + } else { + this.setStatus(Status.Idle) + } + + this.app.cancelSession() + } +} diff --git a/packages/tldraw/src/state/tools/EraseTool/index.ts b/packages/tldraw/src/state/tools/EraseTool/index.ts new file mode 100644 index 000000000..a5abc861a --- /dev/null +++ b/packages/tldraw/src/state/tools/EraseTool/index.ts @@ -0,0 +1 @@ +export * from './EraseTool' diff --git a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.spec.ts b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.spec.ts index c56fa17fb..aa4a3f976 100644 --- a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.spec.ts +++ b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { RectangleTool } from '.' describe('RectangleTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new RectangleTool(state) + const app = new TldrawApp() + new RectangleTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts index 62f683fa4..cf88ed6ae 100644 --- a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts +++ b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts @@ -1,20 +1,18 @@ -import Vec from '@tldraw/vec' import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core' import { Rectangle } from '~state/shapes' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class RectangleTool extends BaseTool { - type = TLDrawShapeType.Rectangle + type = TDShapeType.Rectangle as const /* ----------------- Event Handlers ----------------- */ - onPointerDown: TLPointerEventHandler = (info) => { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - + onPointerDown: TLPointerEventHandler = () => { const { + currentPoint, appState: { currentPageId, currentStyle }, - } = this.state + } = this.app const childIndex = this.getNextChildIndex() @@ -24,15 +22,15 @@ export class RectangleTool extends BaseTool { id, parentId: currentPageId, childIndex, - point: pagePoint, + point: currentPoint, style: { ...currentStyle }, }) - this.state.patchCreate([newShape]) + this.app.patchCreate([newShape]) - this.state.startSession( + this.app.startSession( SessionType.TransformSingle, - pagePoint, + newShape.id, TLBoundsCorner.BottomRight, true ) diff --git a/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts b/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts index d17a7d6d8..f54239430 100644 --- a/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts +++ b/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts @@ -1,79 +1,81 @@ -import { TLDrawState } from '~state' -import { mockDocument, TLDrawStateUtils } from '~test' -import { SessionType, TLDrawShapeType } from '~types' +import { mockDocument, TldrawTestApp } from '~test' +import { SessionType, TDShapeType } from '~types' import { SelectTool } from '.' describe('SelectTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new SelectTool(state) + const app = new TldrawTestApp() + new SelectTool(app) }) }) describe('When double clicking link controls', () => { - const doc = new TLDrawState() + const doc = new TldrawTestApp() .createShapes( { id: 'rect1', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], }, { id: 'rect2', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [100, 0], size: [100, 100], }, { id: 'rect3', - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [200, 0], size: [100, 100], }, { id: 'arrow1', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, point: [200, 200], }, { id: 'arrow2', - type: TLDrawShapeType.Arrow, + type: TDShapeType.Arrow, point: [200, 200], } ) .select('arrow1') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([50, 50]) + .movePointer({ x: 200, y: 200 }) + .startSession(SessionType.Arrow, 'arrow1', 'start') + .movePointer({ x: 50, y: 50 }) .completeSession() - .startSession(SessionType.Arrow, [200, 200], 'end') - .updateSession([150, 50]) + .movePointer({ x: 200, y: 200 }) + .startSession(SessionType.Arrow, 'arrow1', 'end') + .movePointer({ x: 150, y: 50 }) .completeSession() .select('arrow2') - .startSession(SessionType.Arrow, [200, 200], 'start') - .updateSession([150, 50]) + .movePointer({ x: 200, y: 200 }) + .startSession(SessionType.Arrow, 'arrow2', 'start') + .movePointer({ x: 150, y: 50 }) .completeSession() - .startSession(SessionType.Arrow, [200, 200], 'end') - .updateSession([250, 50]) + .movePointer({ x: 200, y: 200 }) + .startSession(SessionType.Arrow, 'arrow2', 'end') + .movePointer({ x: 250, y: 50 }) .completeSession() .selectNone().document it('moves all linked shapes when center is dragged', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) + const app = new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .pointBoundsHandle('center', { x: 0, y: 0 }) - tlu - .pointBoundsHandle('center') - .movePointer({ x: 100, y: 100 }) - .expectShapesToBeAtPoints({ - rect1: [100, 100], - rect2: [200, 100], - rect3: [300, 100], - }) + app.movePointer({ x: 100, y: 100 }).expectShapesToBeAtPoints({ + rect1: [100, 100], + rect2: [200, 100], + rect3: [300, 100], + }) - state.completeSession().undo() + app.completeSession().undo() - tlu.expectShapesToBeAtPoints({ + app.expectShapesToBeAtPoints({ rect1: [0, 0], rect2: [100, 0], rect3: [200, 0], @@ -81,71 +83,73 @@ describe('When double clicking link controls', () => { }) it('moves all upstream shapes when center is dragged', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) + const app = new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .pointBoundsHandle('center') + .movePointer({ x: 100, y: 100 }) - tlu.pointBoundsHandle('left').movePointer({ x: 100, y: 100 }) - - expect(state.getShape('rect1').point).toEqual([100, 100]) - expect(state.getShape('rect2').point).toEqual([200, 100]) - expect(state.getShape('rect3').point).toEqual([200, 0]) + expect(app.getShape('rect1').point).toEqual([100, 100]) + expect(app.getShape('rect2').point).toEqual([200, 100]) + expect(app.getShape('rect3').point).toEqual([300, 100]) }) it('moves all downstream shapes when center is dragged', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) + const app = new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .pointBoundsHandle('right') + .movePointer({ x: 100, y: 100 }) - tlu.pointBoundsHandle('right').movePointer({ x: 100, y: 100 }) - - expect(state.getShape('rect1').point).toEqual([0, 0]) - expect(state.getShape('rect2').point).toEqual([200, 100]) - expect(state.getShape('rect3').point).toEqual([300, 100]) + expect(app.getShape('rect1').point).toEqual([0, 0]) + expect(app.getShape('rect2').point).toEqual([200, 100]) + expect(app.getShape('rect3').point).toEqual([300, 100]) }) it('selects all linked shapes when center is double clicked', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu.doubleClickBoundHandle('center').expectSelectedIdsToBe(['rect2', 'rect1', 'rect3']) + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .doubleClickBoundHandle('center') + .expectSelectedIdsToBe(['rect2', 'rect1', 'rect3']) }) it('selects all linked shapes and arrows when center is double clicked while holding shift', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') .doubleClickBoundHandle('center', { shiftKey: true }) .expectSelectedIdsToBe(['rect2', 'rect1', 'rect3', 'arrow1', 'arrow2']) }) it('selects all upstream linked shapes when left is double clicked', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu.doubleClickBoundHandle('left').expectSelectedIdsToBe(['rect1', 'rect2']) + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .doubleClickBoundHandle('left') + .expectSelectedIdsToBe(['rect1', 'rect2']) }) it('selects all upstream linked shapes and arrows when left is double clicked with shift', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') .doubleClickBoundHandle('left', { shiftKey: true }) .expectSelectedIdsToBe(['rect1', 'rect2', 'arrow1']) }) it('selects all downstream linked shapes when right is double clicked', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu.doubleClickBoundHandle('right').expectSelectedIdsToBe(['rect2', 'rect3']) + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') + .doubleClickBoundHandle('right') + .expectSelectedIdsToBe(['rect2', 'rect3']) }) it('selects all downstream linked shapes and arrows when right is double clicked with shift', () => { - const state = new TLDrawState().loadDocument(doc).select('rect2') - const tlu = new TLDrawStateUtils(state) - - tlu + new TldrawTestApp() + .loadDocument(doc) + .select('rect2') .doubleClickBoundHandle('right', { shiftKey: true }) .expectSelectedIdsToBe(['rect2', 'rect3', 'arrow2']) }) @@ -153,69 +157,74 @@ describe('When double clicking link controls', () => { describe('When selecting grouped shapes', () => { it('Selects the group on single click', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') - new TLDrawStateUtils(state).clickShape('rect1') + .clickShape('rect1') - expect(state.selectedIds).toStrictEqual(['groupA']) + expect(app.selectedIds).toStrictEqual(['groupA']) }) it('Drills in and selects the child on double click', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .doubleClickShape('rect1') - new TLDrawStateUtils(state).doubleClickShape('rect1') - - expect(state.selectedIds).toStrictEqual(['rect1']) + expect(app.selectedIds).toStrictEqual(['rect1']) }) it('Selects a sibling on single click after drilling', () => { - const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA') + const app = new TldrawTestApp() + .loadDocument(mockDocument) + .group(['rect1', 'rect2'], 'groupA') + .doubleClickShape('rect1') + .clickShape('rect2') - new TLDrawStateUtils(state).doubleClickShape('rect1').clickShape('rect2') - - expect(state.selectedIds).toStrictEqual(['rect2']) + expect(app.selectedIds).toStrictEqual(['rect2']) }) it('Selects the group again after selecting a different shape', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .selectAll() .group(['rect1', 'rect2'], 'groupA') + .doubleClickShape('rect1') + .clickShape('rect3') + .clickShape('rect1') - new TLDrawStateUtils(state).doubleClickShape('rect1').clickShape('rect3').clickShape('rect1') - - expect(state.selectedIds).toStrictEqual(['groupA']) + expect(app.selectedIds).toStrictEqual(['groupA']) }) it('Selects grouped text on double click', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .createShapes({ id: 'text1', - type: TLDrawShapeType.Text, + type: TDShapeType.Text, text: 'Hello world', }) .group(['rect1', 'rect2', 'text1'], 'groupA') + .doubleClickShape('text1') - new TLDrawStateUtils(state).doubleClickShape('text1') - - expect(state.selectedIds).toStrictEqual(['text1']) - expect(state.pageState.editingId).toBeUndefined() + expect(app.selectedIds).toStrictEqual(['text1']) + expect(app.pageState.editingId).toBeUndefined() }) it('Edits grouped text on double click after selecting', () => { - const state = new TLDrawState() + const app = new TldrawTestApp() .loadDocument(mockDocument) .createShapes({ id: 'text1', - type: TLDrawShapeType.Text, + type: TDShapeType.Text, text: 'Hello world', }) .group(['rect1', 'rect2', 'text1'], 'groupA') + .doubleClickShape('text1') + .doubleClickShape('text1') - new TLDrawStateUtils(state).doubleClickShape('text1').doubleClickShape('text1') - - expect(state.selectedIds).toStrictEqual(['text1']) - expect(state.pageState.editingId).toBe('text1') + expect(app.selectedIds).toStrictEqual(['text1']) + expect(app.pageState.editingId).toBe('text1') }) }) diff --git a/packages/tldraw/src/state/tools/SelectTool/SelectTool.ts b/packages/tldraw/src/state/tools/SelectTool/SelectTool.ts index f21142a49..157363dd3 100644 --- a/packages/tldraw/src/state/tools/SelectTool/SelectTool.ts +++ b/packages/tldraw/src/state/tools/SelectTool/SelectTool.ts @@ -9,11 +9,11 @@ import { TLShapeCloneHandler, Utils, } from '@tldraw/core' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool } from '../BaseTool' import Vec from '@tldraw/vec' import { TLDR } from '~state/TLDR' -import { CLONING_DISTANCE } from '~constants' +import { CLONING_DISTANCE, DEAD_ZONE } from '~constants' enum Status { Idle = 'idle', @@ -51,20 +51,20 @@ export class SelectTool extends BaseTool { /* --------------------- Methods -------------------- */ private deselect(id: string) { - this.state.select(...this.state.selectedIds.filter((oid) => oid !== id)) + this.app.select(...this.app.selectedIds.filter((oid) => oid !== id)) } private select(id: string) { - this.state.select(id) + this.app.select(id) } private pushSelect(id: string) { - const shape = this.state.getShape(id) - this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id) + const shape = this.app.getShape(id) + this.app.select(...this.app.selectedIds.filter((oid) => oid !== shape.parentId), id) } private selectNone() { - this.state.selectNone() + this.app.selectNone() } onEnter = () => { @@ -76,9 +76,9 @@ export class SelectTool extends BaseTool { } clonePaint = (point: number[]) => { - if (this.state.selectedIds.length === 0) return + if (this.app.selectedIds.length === 0) return - const shapes = this.state.selectedIds.map((id) => this.state.getShape(id)) + const shapes = this.app.selectedIds.map((id) => this.app.getShape(id)) const bounds = Utils.expandBounds(Utils.getCommonBounds(shapes.map(TLDR.getBounds)), 16) @@ -93,12 +93,12 @@ export class SelectTool extends BaseTool { const centeredBounds = Utils.centerBounds(bounds, gridPoint) - const hit = this.state.shapes.some((shape) => - TLDR.getShapeUtils(shape).hitTestBounds(shape, centeredBounds) + const hit = this.app.shapes.some((shape) => + TLDR.getShapeUtil(shape).hitTestBounds(shape, centeredBounds) ) if (!hit) { - this.state.duplicate(this.state.selectedIds, gridPoint) + this.app.duplicate(this.app.selectedIds, gridPoint) } } @@ -114,9 +114,9 @@ export class SelectTool extends BaseTool { | 'bottomRight' | 'bottomLeft' ) => { - const shape = this.state.getShape(id) + const shape = this.app.getShape(id) - const utils = TLDR.getShapeUtils(shape) + const utils = TLDR.getShapeUtil(shape) if (utils.canClone) { const bounds = utils.getBounds(shape) @@ -159,7 +159,7 @@ export class SelectTool extends BaseTool { point, } - if (clone.type === TLDrawShapeType.Sticky) { + if (clone.type === TDShapeType.Sticky) { clone.text = '' } @@ -173,11 +173,11 @@ export class SelectTool extends BaseTool { onCancel = () => { this.selectNone() - this.state.cancelSession() + this.app.cancelSession() this.setStatus(Status.Idle) } - onKeyDown: TLKeyboardEventHandler = (key, info) => { + onKeyDown: TLKeyboardEventHandler = (key) => { if (key === 'Escape') { this.onCancel() return @@ -188,17 +188,17 @@ export class SelectTool extends BaseTool { } if (key === 'Tab') { - if (this.status === Status.Idle && this.state.selectedIds.length === 1) { - const [selectedId] = this.state.selectedIds + if (this.status === Status.Idle && this.app.selectedIds.length === 1) { + const [selectedId] = this.app.selectedIds const clonedShape = this.getShapeClone(selectedId, 'right') if (clonedShape) { - this.state.createShapes(clonedShape) + this.app.createShapes(clonedShape) this.setStatus(Status.Idle) - this.state.setEditingId(clonedShape.id) - this.state.select(clonedShape.id) + this.app.setEditingId(clonedShape.id) + this.app.select(clonedShape.id) } } @@ -206,12 +206,7 @@ export class SelectTool extends BaseTool { } if (key === 'Meta' || key === 'Control' || key === 'Alt') { - this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) + this.app.updateSession() return } } @@ -229,12 +224,7 @@ export class SelectTool extends BaseTool { /* noop */ if (key === 'Meta' || key === 'Control' || key === 'Alt') { - this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) + this.app.updateSession() return } } @@ -244,131 +234,119 @@ export class SelectTool extends BaseTool { // Pointer Events (generic) onPointerMove: TLPointerEventHandler = (info, e) => { - const point = this.state.getPagePoint(info.origin) + const { originPoint, currentPoint } = this.app if (this.status === Status.SpacePanning && e.buttons === 1) { - this.state.onPan?.({ ...info, delta: Vec.neg(info.delta) }, e as unknown as WheelEvent) + this.app.onPan?.({ ...info, delta: Vec.neg(info.delta) }, e as unknown as WheelEvent) return } if (this.status === Status.PointingBoundsHandle) { if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle') - if (Vec.dist(info.origin, info.point) > 4) { + if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) { if (this.pointedBoundsHandle === 'rotate') { // Stat a rotate session this.setStatus(Status.Rotating) - this.state.startSession(SessionType.Rotate, point) + this.app.startSession(SessionType.Rotate) } else if ( this.pointedBoundsHandle === 'center' || this.pointedBoundsHandle === 'left' || this.pointedBoundsHandle === 'right' ) { - const point = this.state.getPagePoint(info.origin) this.setStatus(Status.Translating) - this.state.startSession(SessionType.Translate, point, false, this.pointedBoundsHandle) + this.app.startSession(SessionType.Translate, false, this.pointedBoundsHandle) } else { // Stat a transform session this.setStatus(Status.Transforming) - const idsToTransform = this.state.selectedIds.flatMap((id) => - TLDR.getDocumentBranch(this.state.state, id, this.state.currentPageId) + const idsToTransform = this.app.selectedIds.flatMap((id) => + TLDR.getDocumentBranch(this.app.state, id, this.app.currentPageId) ) if (idsToTransform.length === 1) { // if only one shape is selected, transform single - this.state.startSession(SessionType.TransformSingle, point, this.pointedBoundsHandle) + this.app.startSession( + SessionType.TransformSingle, + idsToTransform[0], + this.pointedBoundsHandle + ) } else { // otherwise, transform - this.state.startSession(SessionType.Transform, point, this.pointedBoundsHandle) + this.app.startSession(SessionType.Transform, this.pointedBoundsHandle) } } // Also update the session with the current point - this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) + this.app.updateSession() } return } if (this.status === Status.PointingCanvas) { - if (Vec.dist(info.origin, info.point) > 4) { - const point = this.state.getPagePoint(info.point) - this.state.startSession(SessionType.Brush, point) + if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) { + this.app.startSession(SessionType.Brush) this.setStatus(Status.Brushing) } return } if (this.status === Status.PointingClone) { - if (Vec.dist(info.origin, info.point) > 4) { + if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) { this.setStatus(Status.TranslatingClone) - const point = this.state.getPagePoint(info.origin) - this.state.startSession(SessionType.Translate, point, true) + this.app.startSession(SessionType.Translate) + this.app.updateSession() } return } if (this.status === Status.PointingBounds) { - if (Vec.dist(info.origin, info.point) > 4) { + if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) { this.setStatus(Status.Translating) - const point = this.state.getPagePoint(info.origin) - this.state.startSession(SessionType.Translate, point) + this.app.startSession(SessionType.Translate) + this.app.updateSession() } return } if (this.status === Status.PointingHandle) { if (!this.pointedHandleId) throw Error('No pointed handle') - if (Vec.dist(info.origin, info.point) > 4) { + if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) { this.setStatus(Status.TranslatingHandle) - - const selectedShape = this.state.getShape(this.state.selectedIds[0]) - + const selectedShape = this.app.getShape(this.app.selectedIds[0]) if (!selectedShape) return - - const point = this.state.getPagePoint(info.point) - if (this.pointedHandleId === 'bend') { - this.state.startSession(SessionType.Handle, point, this.pointedHandleId) + this.app.startSession(SessionType.Handle, selectedShape.id, this.pointedHandleId) + this.app.updateSession() } else { - this.state.startSession(SessionType.Arrow, point, this.pointedHandleId, false) + this.app.startSession(SessionType.Arrow, selectedShape.id, this.pointedHandleId, false) + this.app.updateSession() } } return } - if (this.state.session) { - return this.state.updateSession( - this.state.getPagePoint(info.point), - info.shiftKey, - info.altKey, - info.metaKey - ) + if (this.app.session) { + return this.app.updateSession() } if (this.status === Status.ClonePainting) { - const point = this.state.getPagePoint(info.point) - this.clonePaint(point) + this.clonePaint(currentPoint) } return } onPointerDown: TLPointerEventHandler = () => { - if (this.state.appState.isStyleOpen) { - this.state.toggleStylePanel() + if (this.app.appState.isStyleOpen) { + this.app.toggleStylePanel() } } onPointerUp: TLPointerEventHandler = (info) => { if (this.status === Status.TranslatingClone || this.status === Status.PointingClone) { if (this.pointedId) { - this.state.completeSession() - this.state.setEditingId(this.pointedId) + this.app.completeSession() + this.app.setEditingId(this.pointedId) } this.setStatus(Status.Idle) this.pointedId = undefined @@ -380,7 +358,7 @@ export class SelectTool extends BaseTool { // If we just clicked the selecting bounds's background, // clear the selection this.selectNone() - } else if (this.state.isSelected(info.target)) { + } else if (this.app.isSelected(info.target)) { // If we're holding shift... if (info.shiftKey) { // unless we just shift-selected the shape, remove it from @@ -390,11 +368,12 @@ export class SelectTool extends BaseTool { } } else { // If we have other selected shapes, select this one instead - if (this.pointedId !== info.target && this.state.selectedIds.length > 1) { + if (this.pointedId !== info.target && this.app.selectedIds.length > 1) { this.select(info.target) } } } else if (this.pointedId === info.target) { + if (this.app.getShape(info.target).isLocked) return // If the target is not selected and was just pointed // on pointer down... if (info.shiftKey) { @@ -406,7 +385,7 @@ export class SelectTool extends BaseTool { } // Complete the current session, if any; and reset the status - this.state.completeSession() + this.app.completeSession() this.setStatus(Status.Idle) this.pointedBoundsHandle = undefined this.pointedHandleId = undefined @@ -416,23 +395,22 @@ export class SelectTool extends BaseTool { // Canvas onPointCanvas: TLCanvasEventHandler = (info, e) => { - if (info.spaceKey && e.buttons === 1) { - return - } + const { currentPoint } = this.app + + if (info.spaceKey && e.buttons === 1) return if (this.status === Status.Idle && info.altKey && info.shiftKey) { this.setStatus(Status.ClonePainting) - const point = this.state.getPagePoint(info.point) - this.clonePaint(point) + this.clonePaint(currentPoint) return } // Unless the user is holding shift or meta, clear the current selection if (!info.shiftKey) { - this.state.onShapeBlur() + this.app.onShapeBlur() - if (info.altKey && this.state.selectedIds.length > 0) { - this.state.duplicate(this.state.selectedIds, this.state.getPagePoint(info.point)) + if (info.altKey && this.app.selectedIds.length > 0) { + this.app.duplicate(this.app.selectedIds, currentPoint) return } @@ -443,24 +421,24 @@ export class SelectTool extends BaseTool { } onDoubleClickCanvas: TLCanvasEventHandler = () => { - // Not working on mobile - // const pagePoint = this.state.getPagePoint(info.point) - // this.state.selectTool(TLDrawShapeType.Text) + // Needs debugging + // const { currentPoint } = this.app + // this.app.selectTool(TDShapeType.Text) // this.setStatus(Status.Idle) - // this.state.createTextShapeAtPoint(pagePoint) + // this.app.createTextShapeAtPoint(currentPoint) } // Shape onPointShape: TLPointerEventHandler = (info, e) => { - if (info.spaceKey && e.buttons === 1) { - return - } + if (info.spaceKey && e.buttons === 1) return - const { editingId, hoveredId } = this.state.pageState + if (this.app.getShape(info.target).isLocked) return + + const { editingId, hoveredId } = this.app.pageState if (editingId && info.target !== editingId) { - this.state.onShapeBlur() + this.app.onShapeBlur() } // While holding command and shift, select or deselect @@ -473,7 +451,7 @@ export class SelectTool extends BaseTool { ) { this.pointedId = hoveredId - if (this.state.isSelected(hoveredId)) { + if (this.app.isSelected(hoveredId)) { this.deselect(hoveredId) } else { this.pushSelect(hoveredId) @@ -486,8 +464,8 @@ export class SelectTool extends BaseTool { if (this.status === Status.PointingBounds) { // The pointed id should be the shape's group, if it belongs // to a group, or else the shape itself, if it is on the page. - const { parentId } = this.state.getShape(info.target) - this.pointedId = parentId === this.state.currentPageId ? info.target : parentId + const { parentId } = this.app.getShape(info.target) + this.pointedId = parentId === this.app.currentPageId ? info.target : parentId return } @@ -499,8 +477,7 @@ export class SelectTool extends BaseTool { this.selectNone() } - const point = this.state.getPagePoint(info.point) - this.state.startSession(SessionType.Brush, point) + this.app.startSession(SessionType.Brush) this.setStatus(Status.Brushing) return @@ -509,11 +486,11 @@ export class SelectTool extends BaseTool { // If we've clicked on a shape that is inside of a group, // then select the group rather than the shape. let shapeIdToSelect: string - const { parentId } = this.state.getShape(info.target) + const { parentId } = this.app.getShape(info.target) // If the pointed shape is a child of the page, select the // target shape and clear the selected group id. - if (parentId === this.state.currentPageId) { + if (parentId === this.app.currentPageId) { shapeIdToSelect = info.target this.selectedGroupId = undefined } else { @@ -531,7 +508,7 @@ export class SelectTool extends BaseTool { } } - if (!this.state.isSelected(shapeIdToSelect)) { + if (!this.app.isSelected(shapeIdToSelect)) { // Set the pointed ID to the shape that was clicked. this.pointedId = shapeIdToSelect @@ -548,46 +525,51 @@ export class SelectTool extends BaseTool { } onDoubleClickShape: TLPointerEventHandler = (info) => { - const shape = this.state.getShape(info.target) + const shape = this.app.getShape(info.target) + + if (shape.isLocked) { + this.app.select(info.target) + return + } // If we can edit the shape (and if we can select the shape) then // start editing if ( - TLDR.getShapeUtils(shape.type).canEdit && - (shape.parentId === this.state.currentPageId || shape.parentId === this.selectedGroupId) + TLDR.getShapeUtil(shape.type).canEdit && + (shape.parentId === this.app.currentPageId || shape.parentId === this.selectedGroupId) ) { - this.state.setEditingId(info.target) + this.app.setEditingId(info.target) } // If the shape is the child of a group, then drill into the group? - if (shape.parentId !== this.state.currentPageId) { + if (shape.parentId !== this.app.currentPageId) { this.selectedGroupId = shape.parentId } - this.state.select(info.target) + this.app.select(info.target) } onRightPointShape: TLPointerEventHandler = (info) => { - if (!this.state.isSelected(info.target)) { - this.state.select(info.target) + if (!this.app.isSelected(info.target)) { + this.app.select(info.target) } } onHoverShape: TLPointerEventHandler = (info) => { - this.state.setHoveredId(info.target) + this.app.setHoveredId(info.target) } onUnhoverShape: TLPointerEventHandler = (info) => { - const { currentPageId: oldCurrentPageId } = this.state + const { currentPageId: oldCurrentPageId } = this.app // Wait a frame; and if we haven't changed the hovered id, // clear the current hovered id requestAnimationFrame(() => { if ( - oldCurrentPageId === this.state.currentPageId && - this.state.pageState.hoveredId === info.target + oldCurrentPageId === this.app.currentPageId && + this.app.pageState.hoveredId === info.target ) { - this.state.setHoveredId(undefined) + this.app.setHoveredId(undefined) } }) } @@ -600,8 +582,7 @@ export class SelectTool extends BaseTool { this.selectNone() } - const point = this.state.getPagePoint(info.point) - this.state.startSession(SessionType.Brush, point) + this.app.startSession(SessionType.Brush) this.setStatus(Status.Brushing) return @@ -612,7 +593,7 @@ export class SelectTool extends BaseTool { onReleaseBounds: TLBoundsEventHandler = () => { if (this.status === Status.Translating || this.status === Status.Brushing) { - this.state.completeSession() + this.app.completeSession() } this.setStatus(Status.Idle) @@ -627,18 +608,18 @@ export class SelectTool extends BaseTool { onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = (info) => { if (info.target === 'center' || info.target === 'left' || info.target === 'right') { - this.state.select( - ...TLDR.getLinkedShapes( - this.state.state, - this.state.currentPageId, + this.app.select( + ...TLDR.getLinkedShapeIds( + this.app.state, + this.app.currentPageId, info.target, info.shiftKey ) ) } - if (this.state.selectedIds.length === 1) { - this.state.resetBounds(this.state.selectedIds) + if (this.app.selectedIds.length === 1) { + this.app.resetBounds(this.app.selectedIds) } } @@ -654,7 +635,7 @@ export class SelectTool extends BaseTool { } onDoubleClickHandle: TLPointerEventHandler = (info) => { - this.state.toggleDecoration(info.target) + this.app.toggleDecoration(info.target) } onReleaseHandle: TLPointerEventHandler = () => { @@ -664,7 +645,7 @@ export class SelectTool extends BaseTool { /* ---------------------- Misc ---------------------- */ onShapeClone: TLShapeCloneHandler = (info) => { - const selectedShapeId = this.state.selectedIds[0] + const selectedShapeId = this.app.selectedIds[0] const clonedShape = this.getShapeClone(selectedShapeId, info.target) @@ -675,7 +656,7 @@ export class SelectTool extends BaseTool { info.target === 'bottom' ) { if (clonedShape) { - this.state.createShapes(clonedShape) + this.app.createShapes(clonedShape) // Now start pointing the bounds, so that a user can start // dragging to reposition if they wish. @@ -683,9 +664,8 @@ export class SelectTool extends BaseTool { this.setStatus(Status.PointingClone) } } else { - const point = this.state.getPagePoint(info.point) this.setStatus(Status.GridCloning) - this.state.startSession(SessionType.Grid, selectedShapeId, this.state.currentPageId, point) + this.app.startSession(SessionType.Grid, selectedShapeId) } } } diff --git a/packages/tldraw/src/state/tools/StickyTool/StickyTool.spec.ts b/packages/tldraw/src/state/tools/StickyTool/StickyTool.spec.ts index a092cc22f..a7dadf533 100644 --- a/packages/tldraw/src/state/tools/StickyTool/StickyTool.spec.ts +++ b/packages/tldraw/src/state/tools/StickyTool/StickyTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { StickyTool } from '.' describe('StickyTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new StickyTool(state) + const app = new TldrawApp() + new StickyTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts b/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts index dbc2ad0fd..b892e1ea2 100644 --- a/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts +++ b/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts @@ -2,33 +2,32 @@ import Vec from '@tldraw/vec' import type { TLPointerEventHandler } from '@tldraw/core' import { Utils } from '@tldraw/core' import { Sticky } from '~state/shapes' -import { SessionType, TLDrawShapeType } from '~types' +import { SessionType, TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class StickyTool extends BaseTool { - type = TLDrawShapeType.Sticky + type = TDShapeType.Sticky as const shapeId?: string /* ----------------- Event Handlers ----------------- */ - onPointerDown: TLPointerEventHandler = (info) => { + onPointerDown: TLPointerEventHandler = () => { if (this.status === Status.Creating) { this.setStatus(Status.Idle) - if (!this.state.appState.isToolLocked) { - this.state.selectTool('select') + if (!this.app.appState.isToolLocked) { + this.app.selectTool('select') } return } if (this.status === Status.Idle) { - const pagePoint = Vec.round(this.state.getPagePoint(info.point)) - const { + currentPoint, appState: { currentPageId, currentStyle }, - } = this.state + } = this.app const childIndex = this.getNextChildIndex() @@ -40,7 +39,7 @@ export class StickyTool extends BaseTool { id, parentId: currentPageId, childIndex, - point: pagePoint, + point: currentPoint, style: { ...currentStyle }, }) @@ -48,9 +47,9 @@ export class StickyTool extends BaseTool { newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2]) - this.state.createShapes(newShape) + this.app.createShapes(newShape) - this.state.startSession(SessionType.Translate, pagePoint) + this.app.startSession(SessionType.Translate) this.setStatus(Status.Creating) } @@ -59,9 +58,9 @@ export class StickyTool extends BaseTool { onPointerUp: TLPointerEventHandler = () => { if (this.status === Status.Creating) { this.setStatus(Status.Idle) - this.state.completeSession() - this.state.selectTool('select') - this.state.setEditingId(this.shapeId) + this.app.completeSession() + this.app.selectTool('select') + this.app.setEditingId(this.shapeId) } } } diff --git a/packages/tldraw/src/state/tools/TextTool/TextTool.spec.ts b/packages/tldraw/src/state/tools/TextTool/TextTool.spec.ts index 187d2b544..ac9ddaa64 100644 --- a/packages/tldraw/src/state/tools/TextTool/TextTool.spec.ts +++ b/packages/tldraw/src/state/tools/TextTool/TextTool.spec.ts @@ -1,9 +1,9 @@ -import { TLDrawState } from '~state' +import { TldrawApp } from '~state' import { TextTool } from '.' describe('TextTool', () => { it('creates tool', () => { - const state = new TLDrawState() - new TextTool(state) + const app = new TldrawApp() + new TextTool(app) }) }) diff --git a/packages/tldraw/src/state/tools/TextTool/TextTool.ts b/packages/tldraw/src/state/tools/TextTool/TextTool.ts index 44bb905af..ef00a830f 100644 --- a/packages/tldraw/src/state/tools/TextTool/TextTool.ts +++ b/packages/tldraw/src/state/tools/TextTool/TextTool.ts @@ -1,18 +1,17 @@ -import Vec from '@tldraw/vec' import type { TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core' -import { TLDrawShapeType } from '~types' +import { TDShapeType } from '~types' import { BaseTool, Status } from '../BaseTool' export class TextTool extends BaseTool { - type = TLDrawShapeType.Text + type = TDShapeType.Text as const /* --------------------- Methods -------------------- */ stopEditingShape = () => { this.setStatus(Status.Idle) - if (!this.state.appState.isToolLocked) { - this.state.selectTool('select') + if (!this.app.appState.isToolLocked) { + this.app.selectTool('select') } } @@ -26,29 +25,30 @@ export class TextTool extends BaseTool { // noop } - onPointerDown: TLPointerEventHandler = (info) => { + onPointerDown: TLPointerEventHandler = () => { if (this.status === Status.Creating) { this.stopEditingShape() return } if (this.status === Status.Idle) { - const point = Vec.round(this.state.getPagePoint(info.point)) - this.state.createTextShapeAtPoint(point) + const { currentPoint } = this.app + this.app.createTextShapeAtPoint(currentPoint) this.setStatus(Status.Creating) return } } onPointerUp: TLPointerEventHandler = () => { - // noop important! + // noop important! We don't want the inherited event + // from BaseUtil to run. } onPointShape: TLPointerEventHandler = (info) => { - const shape = this.state.getShape(info.target) - if (shape.type === TLDrawShapeType.Text) { + const shape = this.app.getShape(info.target) + if (shape.type === TDShapeType.Text) { this.setStatus(Status.Idle) - this.state.setEditingId(shape.id) + this.app.setEditingId(shape.id) } } diff --git a/packages/tldraw/src/state/tools/about-tools.md b/packages/tldraw/src/state/tools/about-tools.md index 7a3749ecf..deba3f72a 100644 --- a/packages/tldraw/src/state/tools/about-tools.md +++ b/packages/tldraw/src/state/tools/about-tools.md @@ -1,6 +1,6 @@ # Tools -Tools are classes that handle events. A TLDrawState instance has a set of tools (`tools`) and one current tool (`currentTool`). The state delegates events (such as `onPointerMove`) to its current tool for handling. +Tools are classes that handle events. A tldrawApp instance has a set of tools (`tools`) and one current tool (`currentTool`). The state delegates events (such as `onPointerMove`) to its current tool for handling. In this way, tools function as a finite state machine: events are always handled by a tool and will only ever be handled by one tool. diff --git a/packages/tldraw/src/state/tools/index.ts b/packages/tldraw/src/state/tools/index.ts index d96b19f5a..955e07721 100644 --- a/packages/tldraw/src/state/tools/index.ts +++ b/packages/tldraw/src/state/tools/index.ts @@ -1,6 +1,4 @@ -import type { TLDrawState } from '~state' -import { TLDrawShapeType, TLDrawToolType } from '~types' -import type { BaseTool } from './BaseTool' +import { TDShapeType, TDToolType } from '~types' import { ArrowTool } from './ArrowTool' import { DrawTool } from './DrawTool' import { EllipseTool } from './EllipseTool' @@ -8,43 +6,30 @@ import { RectangleTool } from './RectangleTool' import { SelectTool } from './SelectTool' import { StickyTool } from './StickyTool' import { TextTool } from './TextTool' +import { EraseTool } from './EraseTool' export interface ToolsMap { select: typeof SelectTool - [TLDrawShapeType.Text]: typeof TextTool - [TLDrawShapeType.Draw]: typeof DrawTool - [TLDrawShapeType.Ellipse]: typeof EllipseTool - [TLDrawShapeType.Rectangle]: typeof RectangleTool - [TLDrawShapeType.Arrow]: typeof ArrowTool - [TLDrawShapeType.Sticky]: typeof StickyTool + erase: typeof EraseTool + [TDShapeType.Text]: typeof TextTool + [TDShapeType.Draw]: typeof DrawTool + [TDShapeType.Ellipse]: typeof EllipseTool + [TDShapeType.Rectangle]: typeof RectangleTool + [TDShapeType.Arrow]: typeof ArrowTool + [TDShapeType.Sticky]: typeof StickyTool } -export type ToolOfType = ToolsMap[K] +export type ToolOfType = ToolsMap[K] -export type ArgsOfType = ConstructorParameters> +export type ArgsOfType = ConstructorParameters> -export const tools: { [K in TLDrawToolType]: ToolsMap[K] } = { +export const tools: { [K in TDToolType]: ToolsMap[K] } = { select: SelectTool, - [TLDrawShapeType.Text]: TextTool, - [TLDrawShapeType.Draw]: DrawTool, - [TLDrawShapeType.Ellipse]: EllipseTool, - [TLDrawShapeType.Rectangle]: RectangleTool, - [TLDrawShapeType.Arrow]: ArrowTool, - [TLDrawShapeType.Sticky]: StickyTool, -} - -export const getTool = (type: K): ToolOfType => { - return tools[type] -} - -export function createTools(state: TLDrawState): Record { - return { - select: new SelectTool(state), - [TLDrawShapeType.Text]: new TextTool(state), - [TLDrawShapeType.Draw]: new DrawTool(state), - [TLDrawShapeType.Ellipse]: new EllipseTool(state), - [TLDrawShapeType.Rectangle]: new RectangleTool(state), - [TLDrawShapeType.Arrow]: new ArrowTool(state), - [TLDrawShapeType.Sticky]: new StickyTool(state), - } + erase: EraseTool, + [TDShapeType.Text]: TextTool, + [TDShapeType.Draw]: DrawTool, + [TDShapeType.Ellipse]: EllipseTool, + [TDShapeType.Rectangle]: RectangleTool, + [TDShapeType.Arrow]: ArrowTool, + [TDShapeType.Sticky]: StickyTool, } diff --git a/packages/tldraw/src/styles/stitches.config.ts b/packages/tldraw/src/styles/stitches.config.ts index 30aabca23..2a800f7ca 100644 --- a/packages/tldraw/src/styles/stitches.config.ts +++ b/packages/tldraw/src/styles/stitches.config.ts @@ -6,33 +6,21 @@ const { styled, createTheme } = createStitches({ }, theme: { colors: { - codeHl: 'rgba(144, 144, 144, .15)', - brushFill: 'rgba(0,0,0,.05)', - brushStroke: 'rgba(0,0,0,.25)', - hint: 'rgba(216, 226, 249, 1.000)', - selected: 'rgba(66, 133, 244, 1.000)', bounds: 'rgba(65, 132, 244, 1.000)', boundsBg: 'rgba(65, 132, 244, 0.05)', - highlight: 'rgba(65, 132, 244, 0.15)', + hover: '#ececec', overlay: 'rgba(0, 0, 0, 0.15)', overlayContrast: 'rgba(255, 255, 255, 0.15)', - border: 'rgba(143, 146, 148, 1)', - focused: 'rgb(143, 146, 148, .35)', - blurred: 'rgb(143, 146, 148, .15)', - canvas: '#f8f9fa', panel: '#fefefe', - panelBorder: 'rgba(0, 0, 0, 0.12)', - panelActive: '#fefefe', - inactive: '#cccccf', - hover: '#eaf2ff', + panelContrast: '#ffffff', + selected: 'rgba(66, 133, 244, 1.000)', + selectedContrast: '#fefefe', + sponsor: '#ec6cb9', + sponsorContrast: '#ec6cb944', text: '#333333', - tooltipBg: '#1d1d1d', - tooltipText: '#ffffff', - muted: '#777777', - input: '#f3f3f3', - inputBorder: '#dddddd', + tooltip: '#1d1d1d', + tooltipContrast: '#ffffff', warn: 'rgba(255, 100, 100, 1)', - lineError: 'rgba(255, 0, 0, .1)', }, shadows: { 2: '0px 1px 1px rgba(0, 0, 0, 0.14)', @@ -106,32 +94,18 @@ const { styled, createTheme } = createStitches({ export const dark = createTheme({ colors: { - brushFill: 'rgba(180, 180, 180, .05)', - brushStroke: 'rgba(180, 180, 180, .25)', - hint: 'rgba(216, 226, 249, 1.000)', - selected: 'rgba(38, 150, 255, 1.000)', bounds: 'rgba(38, 150, 255, 1.000)', boundsBg: 'rgba(38, 150, 255, 0.05)', - highlight: 'rgba(38, 150, 255, 0.15)', + hover: '#444A50', overlay: 'rgba(0, 0, 0, 0.15)', overlayContrast: 'rgba(255, 255, 255, 0.15)', - border: 'rgb(32, 37, 41, 1)', - focused: 'rgb(32, 37, 41, 1, .15)', - blurred: 'rgb(32, 37, 41, 1, .05)', - canvas: '#343d45', - panel: '#49555f', - panelBorder: 'rgba(255, 255, 255, 0.12)', - panelActive: '#fefefe', - inactive: '#aaaaad', - hover: '#343d45', + panel: '#363D44', + panelContrast: '#49555f', + selected: 'rgba(38, 150, 255, 1.000)', + selectedContrast: '#fefefe', text: '#f8f9fa', - muted: '#e0e2e6', - input: '#f3f3f3', - inputBorder: '#ddd', - tooltipBg: '#1d1d1d', - tooltipText: '#ffffff', - codeHl: 'rgba(144, 144, 144, .15)', - lineError: 'rgba(255, 0, 0, .1)', + tooltip: '#1d1d1d', + tooltipContrast: '#ffffff', }, shadows: { 2: '0px 1px 1px rgba(0, 0, 0, 0.24)', diff --git a/packages/tldraw/src/test/TLDrawStateUtils.tsx b/packages/tldraw/src/test/TldrawTestApp.tsx similarity index 55% rename from packages/tldraw/src/test/TLDrawStateUtils.tsx rename to packages/tldraw/src/test/TldrawTestApp.tsx index 0c3b3e338..b0a680b6d 100644 --- a/packages/tldraw/src/test/TLDrawStateUtils.tsx +++ b/packages/tldraw/src/test/TldrawTestApp.tsx @@ -1,6 +1,6 @@ import { inputs, TLBoundsEdge, TLBoundsCorner, TLBoundsHandle } from '@tldraw/core' -import type { TLDrawState } from '~state' -import type { TLDrawShape } from '~types' +import { TldrawApp } from '~state' +import type { TDShape } from '~types' interface PointerOptions { id?: number @@ -11,114 +11,102 @@ interface PointerOptions { ctrlKey?: boolean } -export class TLDrawStateUtils { - state: TLDrawState - - constructor(state: TLDrawState) { - this.state = state - } - - movePointer = (options: PointerOptions = {}) => { - const { state } = this - state.onPointerMove(inputs.pointerMove(this.getPoint(options), ''), {} as React.PointerEvent) - return this - } - +export class TldrawTestApp extends TldrawApp { hoverShape = (id: string, options: PointerOptions = {}) => { - const { state } = this - state.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent) + this.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent) return this } - pointCanvas = (options: PointerOptions = {}) => { - this.state.onPointCanvas( + movePointer = (options?: PointerOptions | number[]) => { + this.onPointerMove(inputs.pointerMove(this.getPoint(options), ''), {} as React.PointerEvent) + return this + } + + pointCanvas = (options?: PointerOptions | number[]) => { + this.onPointCanvas( inputs.pointerDown(this.getPoint(options), 'canvas'), {} as React.PointerEvent ) - this.state.onPointerDown( + this.onPointerDown( inputs.pointerDown(this.getPoint(options), 'canvas'), {} as React.PointerEvent ) return this } - pointShape = (id: string, options: PointerOptions = {}) => { - this.state.onPointShape( + pointShape = (id: string, options?: PointerOptions | number[]) => { + this.onPointShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent) + this.onPointerDown( + inputs.pointerDown(this.getPoint(options), 'canvas'), + {} as React.PointerEvent + ) + return this + } + + doubleClickShape = (id: string, options?: PointerOptions | number[]) => { + this.onPointerDown( + inputs.pointerDown(this.getPoint(options), 'canvas'), + {} as React.PointerEvent + ) + this.onDoubleClickShape( inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent ) - this.state.onPointerDown( - inputs.pointerDown(this.getPoint(options), 'canvas'), - {} as React.PointerEvent - ) + this.onPointerUp(inputs.pointerUp(this.getPoint(options), 'canvas'), {} as React.PointerEvent) return this } - doubleClickShape = (id: string, options: PointerOptions = {}) => { - this.state.onDoubleClickShape( - inputs.pointerDown(this.getPoint(options), id), - {} as React.PointerEvent - ) - this.state.onPointerDown( - inputs.pointerDown(this.getPoint(options), 'canvas'), - {} as React.PointerEvent - ) - return this - } - - pointBounds = (options: PointerOptions = {}) => { - this.state.onPointBounds( + pointBounds = (options?: PointerOptions | number[]) => { + this.onPointBounds( inputs.pointerDown(this.getPoint(options), 'bounds'), {} as React.PointerEvent ) - this.state.onPointerDown( + this.onPointerDown( inputs.pointerDown(this.getPoint(options), 'canvas'), {} as React.PointerEvent ) return this } - pointBoundsHandle = (id: TLBoundsHandle, options: PointerOptions = {}) => { - this.state.onPointBoundsHandle( + pointBoundsHandle = (id: TLBoundsHandle, options?: PointerOptions | number[]) => { + this.onPointBoundsHandle( inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent ) - this.state.onPointerDown( + this.onPointerDown( inputs.pointerDown(this.getPoint(options), 'canvas'), {} as React.PointerEvent ) return this } - doubleClickBoundHandle = (id: TLBoundsHandle, options: PointerOptions = {}) => { - this.state.onDoubleClickBoundsHandle( + doubleClickBoundHandle = (id: TLBoundsHandle, options?: PointerOptions | number[]) => { + this.onDoubleClickBoundsHandle( inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent ) - this.state.onPointerDown( + this.onPointerDown( inputs.pointerDown(this.getPoint(options), 'canvas'), {} as React.PointerEvent ) return this } - stopPointing = (target = 'canvas', options: PointerOptions = {}) => { - this.state.onPointerUp( - inputs.pointerUp(this.getPoint(options), target), - {} as React.PointerEvent - ) + stopPointing = (target = 'canvas', options?: PointerOptions | number[]) => { + this.onPointerUp(inputs.pointerUp(this.getPoint(options), target), {} as React.PointerEvent) return this } - clickCanvas = (options: PointerOptions = {}) => { + clickCanvas = (options?: PointerOptions | number[]) => { this.pointCanvas(options) this.stopPointing() return this } - clickShape = (id: string, options: PointerOptions = {}) => { + clickShape = (id: string, options?: PointerOptions | number[]) => { this.hoverShape(id) this.pointShape(id, options) + this.onReleaseShape(inputs.pointerUp(this.getPoint(options), id), {} as React.PointerEvent) this.stopPointing(id, options) return this } @@ -138,8 +126,9 @@ export class TLDrawStateUtils { return this } - getPoint(options: PointerOptions = {} as PointerOptions): PointerEvent { - const { id = 1, x = 0, y = 0, shiftKey = false, altKey = false, ctrlKey = false } = options + getPoint(options: PointerOptions | number[] = {} as PointerOptions): PointerEvent { + const opts = Array.isArray(options) ? { x: options[0], y: options[1] } : options + const { id = 1, x = 0, y = 0, shiftKey = false, altKey = false, ctrlKey = false } = opts return { shiftKey, @@ -152,20 +141,20 @@ export class TLDrawStateUtils { } expectSelectedIdsToBe = (b: string[]) => { - expect(new Set(this.state.selectedIds)).toEqual(new Set(b)) + expect(new Set(this.selectedIds)).toEqual(new Set(b)) return this } expectShapesToBeAtPoints = (shapes: Record) => { Object.entries(shapes).forEach(([id, point]) => { - expect(this.state.getShape(id).point).toEqual(point) + expect(this.getShape(id).point).toEqual(point) }) return this } - expectShapesToHaveProps = (shapes: Record>) => { + expectShapesToHaveProps = (shapes: Record>) => { Object.entries(shapes).forEach(([id, props]) => { - const shape = this.state.getShape(id) + const shape = this.getShape(id) Object.entries(props).forEach(([key, value]) => { expect(shape[key as keyof T]).toEqual(value) }) diff --git a/packages/tldraw/src/test/index.ts b/packages/tldraw/src/test/index.ts index 35a1aec6f..ea4cb1a9e 100644 --- a/packages/tldraw/src/test/index.ts +++ b/packages/tldraw/src/test/index.ts @@ -1,3 +1,3 @@ export * from './mockDocument' export * from './renderWithContext' -export * from './TLDrawStateUtils' +export * from './TldrawTestApp' diff --git a/packages/tldraw/src/test/mockDocument.tsx b/packages/tldraw/src/test/mockDocument.tsx index cce76c3f0..179ae24ed 100644 --- a/packages/tldraw/src/test/mockDocument.tsx +++ b/packages/tldraw/src/test/mockDocument.tsx @@ -1,6 +1,6 @@ -import { TLDrawDocument, ColorStyle, DashStyle, SizeStyle, TLDrawShapeType } from '~types' +import { TDDocument, ColorStyle, DashStyle, SizeStyle, TDShapeType } from '~types' -export const mockDocument: TLDrawDocument = { +export const mockDocument: TDDocument = { version: 0, id: 'doc', name: 'New Document', @@ -13,7 +13,7 @@ export const mockDocument: TLDrawDocument = { parentId: 'page1', name: 'Rectangle', childIndex: 1, - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100], style: { @@ -27,7 +27,7 @@ export const mockDocument: TLDrawDocument = { parentId: 'page1', name: 'Rectangle', childIndex: 2, - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100], style: { @@ -41,7 +41,7 @@ export const mockDocument: TLDrawDocument = { parentId: 'page1', name: 'Rectangle', childIndex: 3, - type: TLDrawShapeType.Rectangle, + type: TDShapeType.Rectangle, point: [20, 20], size: [100, 100], style: { diff --git a/packages/tldraw/src/test/renderWithContext.tsx b/packages/tldraw/src/test/renderWithContext.tsx index 22fa2ff1f..9acbf72a3 100644 --- a/packages/tldraw/src/test/renderWithContext.tsx +++ b/packages/tldraw/src/test/renderWithContext.tsx @@ -1,14 +1,14 @@ import * as React from 'react' import { IdProvider } from '@radix-ui/react-id' -import { TLDrawState } from '~state' -import { useKeyboardShortcuts, TLDrawContext } from '~hooks' +import { TldrawApp } from '~state' +import { useKeyboardShortcuts, TldrawContext } from '~hooks' import { mockDocument } from './mockDocument' import { render } from '@testing-library/react' export const Wrapper: React.FC = ({ children }) => { - const [state] = React.useState(() => new TLDrawState()) + const [app] = React.useState(() => new TldrawApp()) const [context] = React.useState(() => { - return { state, useSelector: state.useStore, callbacks: {} } + return app }) const rWrapper = React.useRef(null) @@ -17,15 +17,15 @@ export const Wrapper: React.FC = ({ children }) => { React.useEffect(() => { if (!document) return - state.loadDocument(mockDocument) - }, [document, state]) + app.loadDocument(mockDocument) + }, [document, app]) return ( - +

    {children}
    - + ) } diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index ac7e94420..269adf769 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -1,26 +1,154 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ +import type { TLPage, TLUser, TLPageState } from '@tldraw/core' +import type { Command, Patch } from 'rko' +import type { FileSystemHandle } from '~state/data/browser-fs-access' import type { TLBinding, TLBoundsCorner, TLBoundsEdge, - TLShapeProps, TLShape, TLHandle, - TLBounds, TLSnapLine, + TLPinchEventHandler, + TLKeyboardEventHandler, + TLPointerEventHandler, + TLWheelEventHandler, + TLCanvasEventHandler, + TLBoundsEventHandler, + TLBoundsHandleEventHandler, + TLShapeBlurHandler, + TLShapeCloneHandler, } from '@tldraw/core' -import type { TLPage, TLUser, TLPageState } from '@tldraw/core' -import type { StoreApi } from 'zustand' -import type { Command, Patch } from 'rko' -import type { FileSystemHandle } from '~state/data/browser-fs-access' -export interface TLDrawHandle extends TLHandle { - canBind?: boolean - bindingId?: string +/* -------------------------------------------------- */ +/* App */ +/* -------------------------------------------------- */ + +// A base class for all classes that handle events from the Renderer, +// including TDApp and all Tools. +export class TDEventHandler { + onPinchStart?: TLPinchEventHandler + onPinchEnd?: TLPinchEventHandler + onPinch?: TLPinchEventHandler + onKeyDown?: TLKeyboardEventHandler + onKeyUp?: TLKeyboardEventHandler + onPointerMove?: TLPointerEventHandler + onPointerUp?: TLPointerEventHandler + onPan?: TLWheelEventHandler + onZoom?: TLWheelEventHandler + onPointerDown?: TLPointerEventHandler + onPointCanvas?: TLCanvasEventHandler + onDoubleClickCanvas?: TLCanvasEventHandler + onRightPointCanvas?: TLCanvasEventHandler + onDragCanvas?: TLCanvasEventHandler + onReleaseCanvas?: TLCanvasEventHandler + onPointShape?: TLPointerEventHandler + onDoubleClickShape?: TLPointerEventHandler + onRightPointShape?: TLPointerEventHandler + onDragShape?: TLPointerEventHandler + onHoverShape?: TLPointerEventHandler + onUnhoverShape?: TLPointerEventHandler + onReleaseShape?: TLPointerEventHandler + onPointBounds?: TLBoundsEventHandler + onDoubleClickBounds?: TLBoundsEventHandler + onRightPointBounds?: TLBoundsEventHandler + onDragBounds?: TLBoundsEventHandler + onHoverBounds?: TLBoundsEventHandler + onUnhoverBounds?: TLBoundsEventHandler + onReleaseBounds?: TLBoundsEventHandler + onPointBoundsHandle?: TLBoundsHandleEventHandler + onDoubleClickBoundsHandle?: TLBoundsHandleEventHandler + onRightPointBoundsHandle?: TLBoundsHandleEventHandler + onDragBoundsHandle?: TLBoundsHandleEventHandler + onHoverBoundsHandle?: TLBoundsHandleEventHandler + onUnhoverBoundsHandle?: TLBoundsHandleEventHandler + onReleaseBoundsHandle?: TLBoundsHandleEventHandler + onPointHandle?: TLPointerEventHandler + onDoubleClickHandle?: TLPointerEventHandler + onRightPointHandle?: TLPointerEventHandler + onDragHandle?: TLPointerEventHandler + onHoverHandle?: TLPointerEventHandler + onUnhoverHandle?: TLPointerEventHandler + onReleaseHandle?: TLPointerEventHandler + onShapeBlur?: TLShapeBlurHandler + onShapeClone?: TLShapeCloneHandler } -export interface TLDrawTransformInfo { +// The shape of the TldrawApp's React (zustand) store +export interface TDSnapshot { + settings: { + isDarkMode: boolean + isDebugMode: boolean + isPenMode: boolean + isReadonlyMode: boolean + isZoomSnap: boolean + nudgeDistanceSmall: number + nudgeDistanceLarge: number + isFocusMode: boolean + isSnapping: boolean + showRotateHandles: boolean + showBindingHandles: boolean + showCloneHandles: boolean + } + appState: { + selectedStyle: ShapeStyles + currentStyle: ShapeStyles + currentPageId: string + pages: Pick, 'id' | 'name' | 'childIndex'>[] + hoveredId?: string + activeTool: TDToolType + isToolLocked: boolean + isStyleOpen: boolean + isEmptyCanvas: boolean + status: string + snapLines: TLSnapLine[] + } + document: TDDocument + room?: { + id: string + userId: string + users: Record + } +} + +export type TldrawPatch = Patch + +export type TldrawCommand = Command + +// The shape of the files stored in JSON +export interface TDFile { + name: string + fileHandle: FileSystemHandle | null + document: TDDocument + assets: Record +} + +// The shape of the Tldraw document +export interface TDDocument { + id: string + name: string + version: number + pages: Record + pageStates: Record +} + +// The shape of a single page in the Tldraw document +export type TDPage = TLPage + +// A partial of a TDPage, used for commands / patches +export type PagePartial = { + shapes: Patch + bindings: Patch +} + +// The meta information passed to TDShapeUtil components +export interface TDMeta { + isDarkMode: boolean +} + +// The type of info given to shapes when transforming +export interface TransformInfo { type: TLBoundsEdge | TLBoundsCorner initialShape: T scaleX: number @@ -28,93 +156,21 @@ export interface TLDrawTransformInfo { transformOrigin: number[] } -// old -export type TLStore = StoreApi - -export type TLChange = TLDrawSnapshot - -export type TLDrawPage = TLPage - -export interface TLDrawDocument { - id: string - name: string - pages: Record - pageStates: Record - version: number -} - -export interface TLDrawSettings { - isDarkMode: boolean - isDebugMode: boolean - isPenMode: boolean - isReadonlyMode: boolean - isZoomSnap: boolean - nudgeDistanceSmall: number - nudgeDistanceLarge: number - isFocusMode: boolean - isSnapping: boolean - showRotateHandles: boolean - showBindingHandles: boolean - showCloneHandles: boolean -} - -export enum TLUserStatus { +// The status of a TDUser +export enum TDUserStatus { Idle = 'idle', Connecting = 'connecting', Connected = 'connected', Disconnected = 'disconnected', } -export interface TLDrawMeta { - isDarkMode: boolean +// A TDUser, for multiplayer rooms +export interface TDUser extends TLUser { + activeShapes: TDShape[] + status: TDUserStatus } -export interface TLDrawUser extends TLUser { - activeShapes: TLDrawShape[] -} - -export type TLDrawShapeProps = TLShapeProps< - T, - E, - TLDrawMeta -> - -export interface TLDrawSnapshot { - settings: TLDrawSettings - appState: { - selectedStyle: ShapeStyles - currentStyle: ShapeStyles - currentPageId: string - pages: Pick, 'id' | 'name' | 'childIndex'>[] - hoveredId?: string - activeTool: TLDrawShapeType | 'select' - isToolLocked: boolean - isStyleOpen: boolean - isEmptyCanvas: boolean - status: string - snapLines: TLSnapLine[] - } - document: TLDrawDocument - room?: { - id: string - userId: string - users: Record - } -} - -export type TLDrawPatch = Patch - -export type TLDrawCommand = Command - -export type PagePartial = { - shapes: Patch - bindings: Patch -} - -export interface SelectHistory { - pointer: number - stack: string[][] -} +export type Theme = 'dark' | 'light' export enum SessionType { Transform = 'transform', @@ -123,47 +179,13 @@ export enum SessionType { Brush = 'brush', Arrow = 'arrow', Draw = 'draw', + Erase = 'erase', Rotate = 'rotate', Handle = 'handle', Grid = 'grid', } -export abstract class Session { - static type: SessionType - abstract status: TLDrawStatus - abstract start: (TLDrawSnapshot: Readonly) => TLDrawPatch | undefined - abstract update: ( - TLDrawSnapshot: Readonly, - point: number[], - shiftKey?: boolean, - altKey?: boolean, - metaKey?: boolean - ) => TLDrawPatch | undefined - abstract complete: ( - TLDrawSnapshot: Readonly - ) => TLDrawPatch | TLDrawCommand | undefined - abstract cancel: (TLDrawSnapshot: Readonly) => TLDrawPatch | undefined - - viewport: TLBounds - - constructor(viewport: TLBounds) { - this.viewport = viewport - } - - updateViewport = (viewport: TLBounds) => { - this.viewport = viewport - } - - static cache: { - selectedIds: string[] - center: number[] - } = { - selectedIds: [], - center: [0, 0], - } -} - -export enum TLDrawStatus { +export enum TDStatus { Idle = 'idle', PointingHandle = 'pointingHandle', PointingBounds = 'pointingBounds', @@ -178,12 +200,36 @@ export enum TLDrawStatus { EditingText = 'editing-text', } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ParametersExceptFirst = F extends (arg0: any, ...rest: infer R) => any ? R : never +export type TDToolType = + | 'select' + | 'erase' + | TDShapeType.Text + | TDShapeType.Draw + | TDShapeType.Ellipse + | TDShapeType.Rectangle + | TDShapeType.Arrow + | TDShapeType.Sticky -export type ExceptFirst = T extends [any, ...infer U] ? U : never - -export type ExceptFirstTwo = T extends [any, any, ...infer U] ? U : never +export type Easing = + | 'linear' + | 'easeInQuad' + | 'easeOutQuad' + | 'easeInOutQuad' + | 'easeInCubic' + | 'easeOutCubic' + | 'easeInOutCubic' + | 'easeInQuart' + | 'easeOutQuart' + | 'easeInOutQuart' + | 'easeInQuint' + | 'easeOutQuint' + | 'easeInOutQuint' + | 'easeInSine' + | 'easeOutSine' + | 'easeInOutSine' + | 'easeInExpo' + | 'easeOutExpo' + | 'easeInOutExpo' export enum MoveType { Backward = 'backward', @@ -216,7 +262,11 @@ export enum FlipType { Vertical = 'vertical', } -export enum TLDrawShapeType { +/* -------------------------------------------------- */ +/* Shapes */ +/* -------------------------------------------------- */ + +export enum TDShapeType { Sticky = 'sticky', Ellipse = 'ellipse', Rectangle = 'rectangle', @@ -230,25 +280,33 @@ export enum Decoration { Arrow = 'arrow', } -export interface TLDrawBaseShape extends TLShape { +export interface TDBaseShape extends TLShape { style: ShapeStyles - type: TLDrawShapeType - handles?: Record + type: TDShapeType + handles?: Record } -export interface DrawShape extends TLDrawBaseShape { - type: TLDrawShapeType.Draw +// The shape created with the draw tool +export interface DrawShape extends TDBaseShape { + type: TDShapeType.Draw points: number[][] isComplete: boolean } -export interface ArrowShape extends TLDrawBaseShape { - type: TLDrawShapeType.Arrow +// The extended handle (used for arrows) +export interface TldrawHandle extends TLHandle { + canBind?: boolean + bindingId?: string +} + +// The shape created with the arrow tool +export interface ArrowShape extends TDBaseShape { + type: TDShapeType.Arrow bend: number handles: { - start: TLDrawHandle - bend: TLDrawHandle - end: TLDrawHandle + start: TldrawHandle + bend: TldrawHandle + end: TldrawHandle } decorations?: { start?: Decoration @@ -257,34 +315,48 @@ export interface ArrowShape extends TLDrawBaseShape { } } -export interface EllipseShape extends TLDrawBaseShape { - type: TLDrawShapeType.Ellipse +export interface ArrowBinding extends TLBinding { + handleId: keyof ArrowShape['handles'] + distance: number + point: number[] +} + +export type TDBinding = ArrowBinding + +// The shape created by the ellipse tool +export interface EllipseShape extends TDBaseShape { + type: TDShapeType.Ellipse radius: number[] } -export interface RectangleShape extends TLDrawBaseShape { - type: TLDrawShapeType.Rectangle +// The shape created by the rectangle tool +export interface RectangleShape extends TDBaseShape { + type: TDShapeType.Rectangle size: number[] } -export interface TextShape extends TLDrawBaseShape { - type: TLDrawShapeType.Text +// The shape created by the text tool +export interface TextShape extends TDBaseShape { + type: TDShapeType.Text text: string } -export interface GroupShape extends TLDrawBaseShape { - type: TLDrawShapeType.Group +// The shape created by the sticky tool +export interface StickyShape extends TDBaseShape { + type: TDShapeType.Sticky + size: number[] + text: string +} + +// The shape created when multiple shapes are grouped +export interface GroupShape extends TDBaseShape { + type: TDShapeType.Group size: number[] children: string[] } -export interface StickyShape extends TLDrawBaseShape { - type: TLDrawShapeType.Sticky - size: number[] - text: string -} - -export type TLDrawShape = +// A union of all shapes +export type TDShape = | RectangleShape | EllipseShape | DrawShape @@ -293,13 +365,7 @@ export type TLDrawShape = | GroupShape | StickyShape -export interface ArrowBinding extends TLBinding { - handleId: keyof ArrowShape['handles'] - distance: number - point: number[] -} - -export type TLDrawBinding = ArrowBinding +/* ------------------ Shape Styles ------------------ */ export enum ColorStyle { White = 'white', @@ -344,16 +410,21 @@ export type ShapeStyles = { scale?: number } +/* -------------------------------------------------- */ +/* Type Helpers */ +/* -------------------------------------------------- */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ParametersExceptFirst = F extends (arg0: any, ...rest: infer R) => any ? R : never + +export type ExceptFirst = T extends [any, ...infer U] ? U : never + +export type ExceptFirstTwo = T extends [any, any, ...infer U] ? U : never + export type PropsOfType = { // eslint-disable-next-line @typescript-eslint/no-explicit-any - [K in keyof TLDrawShape]: TLDrawShape[K] extends any - ? TLDrawShape[K] extends U - ? K - : never - : never -}[keyof TLDrawShape] - -export type Theme = 'dark' | 'light' + [K in keyof TDShape]: TDShape[K] extends any ? (TDShape[K] extends U ? K : never) : never +}[keyof TDShape] export type Difference = A extends B ? never : C @@ -375,46 +446,4 @@ export type MappedByType = { [P in T['type']]: T extends any ? (P extends T['type'] ? T : never) : never } -export type ShapesWithProp = MembersWithRequiredKey< - MappedByType, - U -> - -export type Easing = - | 'linear' - | 'easeInQuad' - | 'easeOutQuad' - | 'easeInOutQuad' - | 'easeInCubic' - | 'easeOutCubic' - | 'easeInOutCubic' - | 'easeInQuart' - | 'easeOutQuart' - | 'easeInOutQuart' - | 'easeInQuint' - | 'easeOutQuint' - | 'easeInOutQuint' - | 'easeInSine' - | 'easeOutSine' - | 'easeInOutSine' - | 'easeInExpo' - | 'easeOutExpo' - | 'easeInOutExpo' - -export type TLDrawToolType = - | 'select' - | TLDrawShapeType.Text - | TLDrawShapeType.Draw - | TLDrawShapeType.Ellipse - | TLDrawShapeType.Rectangle - | TLDrawShapeType.Arrow - | TLDrawShapeType.Sticky - -/* ------------------- File System ------------------ */ - -export interface TLDrawFile { - name: string - fileHandle: FileSystemHandle | null - document: TLDrawDocument - assets: Record -} +export type ShapesWithProp = MembersWithRequiredKey, U> diff --git a/tsconfig.base.json b/tsconfig.base.json index 6f9d5e72e..beda42fbd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,7 +8,7 @@ "allowSyntheticDefaultImports": true, "esModuleInterop": true, "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": false, "importHelpers": true, "importsNotUsedAsValues": "error", "resolveJsonModule": true, diff --git a/vscode/editor/README.md b/vscode/editor/README.md index c83eed333..2a0b13ad3 100644 --- a/vscode/editor/README.md +++ b/vscode/editor/README.md @@ -4,6 +4,6 @@ # @tldraw/vscode-editor -The app for the TLDraw VS Code Extension. +The app for the tldraw VS Code Extension. See the README at `vscode` for more about this project. diff --git a/vscode/editor/src/app.tsx b/vscode/editor/src/app.tsx index b9179afeb..6d692e9c5 100644 --- a/vscode/editor/src/app.tsx +++ b/vscode/editor/src/app.tsx @@ -1,33 +1,33 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { TLDraw, TLDrawState, TLDrawFile, TLDrawDocument } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, TDFile, TDDocument } from '@tldraw/tldraw' import { vscode } from './utils/vscode' import { defaultDocument } from './utils/defaultDocument' import type { MessageFromExtension, MessageFromWebview } from './types' // Will be placed in global scope by extension -declare let currentFile: TLDrawFile +declare let currentFile: TDFile export default function App(): JSX.Element { - const rTLDrawState = React.useRef() - const rInitialDocument = React.useRef( + const rTldrawApp = React.useRef() + const rInitialDocument = React.useRef( currentFile ? currentFile.document : defaultDocument ) // When the editor mounts, save the state instance in a ref. - const handleMount = React.useCallback((state: TLDrawState) => { - rTLDrawState.current = state + const handleMount = React.useCallback((state: TldrawApp) => { + rTldrawApp.current = state }, []) // When the editor's document changes, post the stringified document to the vscode extension. - const handlePersist = React.useCallback((state: TLDrawState) => { + const handlePersist = React.useCallback((state: TldrawApp) => { vscode.postMessage({ type: 'editorUpdated', text: JSON.stringify({ ...currentFile, document: state.document, assets: {}, - } as TLDrawFile), + } as TDFile), } as MessageFromWebview) }, []) @@ -36,8 +36,8 @@ export default function App(): JSX.Element { function handleMessage({ data }: MessageEvent) { if (data.type === 'openedFile') { try { - const { document } = JSON.parse(data.text) as TLDrawFile - const state = rTLDrawState.current! + const { document } = JSON.parse(data.text) as TDFile + const state = rTldrawApp.current! state.updateDocument(document) } catch (e) { console.warn('Failed to parse file:', data.text) @@ -54,7 +54,7 @@ export default function App(): JSX.Element { return (
    - **Tip:** The files you create or edit here can also be opened in the TLDraw [web app](https://tldraw.com). +> **Tip:** The files you create or edit here can also be opened in the tldraw [web app](https://tldraw.com). ## Installation -You can install TLDraw from the Visual Studio Code Marketplace or by searching within VS Code. +You can install tldraw from the Visual Studio Code Marketplace or by searching within VS Code. ## Features -1. View, edit and save TLDraw files (`.tldr`) +1. View, edit and save tldraw files (`.tldr`) ## Usage -- To view an existing TLDraw file, open a file with the `.tldr` extension in VS Code. -- To create a new TLDraw file, use the provided command: "TLDraw: New TLDraw File". +- To view an existing tldraw file, open a file with the `.tldr` extension in VS Code. +- To create a new tldraw file, use the provided command: "tldraw: New tldraw File". ## Release Notes diff --git a/vscode/extension/src/TLDrawEditorProvider.ts b/vscode/extension/src/TLDrawEditorProvider.ts index 7ddf811b9..45fdf7c2d 100644 --- a/vscode/extension/src/TLDrawEditorProvider.ts +++ b/vscode/extension/src/TLDrawEditorProvider.ts @@ -1,24 +1,24 @@ import * as vscode from 'vscode' -import { TLDrawWebviewManager } from './TLDrawWebviewManager' +import { TldrawWebviewManager } from './TldrawWebviewManager' /** - * The TLDraw extension's editor uses CustomTextEditorProvider, which means + * The Tldraw extension's editor uses CustomTextEditorProvider, which means * it's underlying model from VS Code's perspective is a text file. We likely * will switch to CustomEditorProvider which gives us more control but will require * more book keeping on our part. */ -export class TLDrawEditorProvider implements vscode.CustomTextEditorProvider { +export class TldrawEditorProvider implements vscode.CustomTextEditorProvider { constructor(private readonly context: vscode.ExtensionContext) {} - private static newTLDrawFileId = 1 + private static newTDFileId = 1 - private static readonly viewType = 'tldraw.tldr' + private static readonly viewType = 'Tldraw.tldr' public static register = (context: vscode.ExtensionContext): vscode.Disposable => { - // Register the 'Create new TLDraw file' command, which creates + // Register the 'Create new Tldraw file' command, which creates // a temporary .tldr file and opens it in the editor. - vscode.commands.registerCommand('tldraw.tldr.new', () => { - const id = TLDrawEditorProvider.newTLDrawFileId++ + vscode.commands.registerCommand('Tldraw.tldr.new', () => { + const id = TldrawEditorProvider.newTDFileId++ const name = id > 1 ? `New Document ${id}.tldr` : `New Document.tldr` const workspaceFolders = vscode.workspace.workspaceFolders @@ -27,15 +27,15 @@ export class TLDrawEditorProvider implements vscode.CustomTextEditorProvider { vscode.commands.executeCommand( 'vscode.openWith', vscode.Uri.joinPath(path, name).with({ scheme: 'untitled' }), - TLDrawEditorProvider.viewType + TldrawEditorProvider.viewType ) }) // Register our editor provider, indicating to VS Code that we can // handle files with the .tldr extension. return vscode.window.registerCustomEditorProvider( - TLDrawEditorProvider.viewType, - new TLDrawEditorProvider(context), + TldrawEditorProvider.viewType, + new TldrawEditorProvider(context), { webviewOptions: { // See https://code.visualstudio.com/api/extension-guides/webview#retaincontextwhenhidden @@ -48,12 +48,12 @@ export class TLDrawEditorProvider implements vscode.CustomTextEditorProvider { ) } - // When our custom editor is opened, create a TLDrawWebviewManager to + // When our custom editor is opened, create a TldrawWebviewManager to // configure the webview and set event listeners to handle events. public async resolveCustomTextEditor( document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel ): Promise { - new TLDrawWebviewManager(this.context, document, webviewPanel) + new TldrawWebviewManager(this.context, document, webviewPanel) } } diff --git a/vscode/extension/src/TLDrawWebviewManager.ts b/vscode/extension/src/TLDrawWebviewManager.ts index 27a87fda6..c519e36cd 100644 --- a/vscode/extension/src/TLDrawWebviewManager.ts +++ b/vscode/extension/src/TLDrawWebviewManager.ts @@ -1,12 +1,12 @@ import * as vscode from 'vscode' -import { TLDrawFile } from '@tldraw/tldraw' +import { TDFile } from '@tldraw/tldraw' import { MessageFromWebview, MessageFromExtension } from './types' /** * When a new editor is opened, an instance of this class will * be created to configure the webview and handle its events. */ -export class TLDrawWebviewManager { +export class TldrawWebviewManager { private disposables: vscode.Disposable[] = [] constructor( @@ -42,7 +42,7 @@ export class TLDrawWebviewManager { this.disposables ) - // Send the initial document content to bootstrap the tldraw/tldraw component. + // Send the initial document content to bootstrap the Tldraw/Tldraw component. webviewPanel.webview.postMessage({ type: 'openedFile', text: document.getText(), @@ -79,13 +79,13 @@ export class TLDrawWebviewManager { switch (e.type) { case 'editorUpdated': { - // The event will contain the new TLDrawFile as JSON. - const nextFile = JSON.parse(e.text) as TLDrawFile + // The event will contain the new TDFile as JSON. + const nextFile = JSON.parse(e.text) as TDFile if (document.getText()) { try { // Parse the contents of the current document. - const currentFile = JSON.parse(document.getText()) as TLDrawFile + const currentFile = JSON.parse(document.getText()) as TDFile // Ensure that the current file's pageStates are preserved // in the next file, unless the associated pages have been deleted. @@ -100,7 +100,7 @@ export class TLDrawWebviewManager { } // Create an edit that replaces the document's current text - // content (a serialized TLDrawFile) with the next file. + // content (a serialized TDFile) with the next file. const edit = new vscode.WorkspaceEdit() edit.replace( @@ -154,7 +154,7 @@ export class TLDrawWebviewManager { - TLDraw + Tldraw
    diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 04ad0b19f..14390e70e 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode' -import { TLDrawEditorProvider } from './TLDrawEditorProvider' +import { TldrawEditorProvider } from './TldrawEditorProvider' // When a .tldr is first opened or created, activate the extension. export function activate(context: vscode.ExtensionContext) { try { - context.subscriptions.push(TLDrawEditorProvider.register(context)) + context.subscriptions.push(TldrawEditorProvider.register(context)) } catch (e) { console.error(e) } diff --git a/www/components/Editor.tsx b/www/components/Editor.tsx new file mode 100644 index 000000000..4962cd3f4 --- /dev/null +++ b/www/components/Editor.tsx @@ -0,0 +1,47 @@ +import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw' +import * as gtag from '-utils/gtag' +import React from 'react' +import { useAccountHandlers } from '-hooks/useAccountHandlers' + +declare const window: Window & { app: TldrawApp } + +interface EditorProps { + id?: string + isUser?: boolean + isSponsor?: boolean +} + +export default function Editor({ id = 'home', isSponsor = false }: EditorProps) { + const handleMount = React.useCallback((app: TldrawApp) => { + window.app = app + }, []) + + // Send events to gtag as actions. + const handlePersist = React.useCallback((_app: TldrawApp, reason?: string) => { + gtag.event({ + action: reason, + category: 'editor', + label: reason || 'persist', + value: 0, + }) + }, []) + + const fileSystemEvents = useFileSystem() + + const { onSignIn, onSignOut } = useAccountHandlers() + + return ( +
    + +
    + ) +} diff --git a/www/components/multiplayer-editor.tsx b/www/components/MultiplayerEditor.tsx similarity index 54% rename from www/components/multiplayer-editor.tsx rename to www/components/MultiplayerEditor.tsx index 1276eba05..2cd25d14b 100644 --- a/www/components/multiplayer-editor.tsx +++ b/www/components/MultiplayerEditor.tsx @@ -1,12 +1,16 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, TDDocument, TDUser, useFileSystem } from '@tldraw/tldraw' import * as React from 'react' import { createClient, Presence } from '@liveblocks/client' import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react' import { Utils } from '@tldraw/core' +import { useAccountHandlers } from '-hooks/useAccountHandlers' +import { styled } from '-styles' -interface TLDrawUserPresence extends Presence { - user: TLDrawUser +declare const window: Window & { app: TldrawApp } + +interface TDUserPresence extends Presence { + user: TDUser } const client = createClient({ @@ -14,20 +18,30 @@ const client = createClient({ throttle: 80, }) -export default function MultiplayerEditor({ roomId }: { roomId: string }) { +export default function MultiplayerEditor({ + roomId, + isUser = false, + isSponsor = false, +}: { + roomId: string + isUser: boolean + isSponsor: boolean +}) { return ( - + ) } -function Editor({ roomId }: { roomId: string }) { +// Inner Editor + +function Editor({ roomId, isSponsor }: { roomId: string; isUser; isSponsor: boolean }) { const [docId] = React.useState(() => Utils.uniqueId()) - const [state, setState] = React.useState() + const [app, setApp] = React.useState() const [error, setError] = React.useState() @@ -35,10 +49,10 @@ function Editor({ roomId }: { roomId: string }) { // Setup document - const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', { + const doc = useObject<{ uuid: string; document: TDDocument }>('doc', { uuid: docId, document: { - ...TLDrawState.defaultDocument, + ...TldrawApp.defaultDocument, id: roomId, }, }) @@ -50,17 +64,16 @@ function Editor({ roomId }: { roomId: string }) { if (!room) return if (!doc) return - if (!state) return - if (!state.state.room) return + if (!app?.room) return // Update the user's presence with the user from state - const { users, userId } = state.state.room + const { users, userId } = app.room room.updatePresence({ id: userId, user: users[userId] }) // Subscribe to presence changes; when others change, update the state - room.subscribe('others', (others) => { - state.updateUsers( + room.subscribe('others', (others) => { + app.updateUsers( others .toArray() .filter((other) => other.presence) @@ -71,23 +84,22 @@ function Editor({ roomId }: { roomId: string }) { room.subscribe('event', (event) => { if (event.event?.name === 'exit') { - state.removeUser(event.event.userId) + app.removeUser(event.event.userId) } }) function handleDocumentUpdates() { if (!doc) return - if (!state) return - if (!state.state.room) return + if (!app?.room) return const docObject = doc.toObject() // Only merge the change if it caused by someone else if (docObject.uuid !== docId) { - state.mergeDocument(docObject.document) + app.mergeDocument(docObject.document) } else { - state.updateUsers( - Object.values(state.state.room.users).map((user) => { + app.updateUsers( + Object.values(app.room.users).map((user) => { return { ...user, selectedIds: user.selectedIds, @@ -98,8 +110,8 @@ function Editor({ roomId }: { roomId: string }) { } function handleExit() { - if (!(state && state.state.room)) return - room?.broadcastEvent({ name: 'exit', userId: state.state.room.userId }) + if (!app?.room) return + room?.broadcastEvent({ name: 'exit', userId: app.room.userId }) } window.addEventListener('beforeunload', handleExit) @@ -111,54 +123,71 @@ function Editor({ roomId }: { roomId: string }) { const newDocument = doc.toObject().document if (newDocument) { - state.loadDocument(newDocument) + app.loadDocument(newDocument) } return () => { window.removeEventListener('beforeunload', handleExit) doc.unsubscribe(handleDocumentUpdates) } - }, [doc, docId, state, roomId]) + }, [doc, docId, app, roomId]) const handleMount = React.useCallback( - (state: TLDrawState) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window.state = state - state.loadRoom(roomId) - setState(state) + (app: TldrawApp) => { + window.app = app + app.loadRoom(roomId) + setApp(app) }, [roomId] ) const handlePersist = React.useCallback( - (state: TLDrawState) => { - doc?.update({ uuid: docId, document: state.document }) + (app: TldrawApp) => { + doc?.update({ uuid: docId, document: app.document }) }, [docId, doc] ) const handleUserChange = React.useCallback( - (state: TLDrawState, user: TLDrawUser) => { + (app: TldrawApp, user: TDUser) => { const room = client.getRoom(roomId) - room?.updatePresence({ id: state.state.room?.userId, user }) + room?.updatePresence({ id: app.room?.userId, user }) }, [roomId] ) - if (error) return
    Error: {error.message}
    + const fileSystemEvents = useFileSystem() - if (doc === null) return
    Loading...
    + const { onSignIn, onSignOut } = useAccountHandlers() + + if (error) return Error: {error.message} + + if (doc === null) return Loading... return (
    -
    ) } + +const LoadingScreen = styled('div', { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}) diff --git a/www/components/editor.tsx b/www/components/editor.tsx deleted file mode 100644 index 32ecb20f3..000000000 --- a/www/components/editor.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { TLDraw, TLDrawState, useFileSystem } from '@tldraw/tldraw' -import { signIn, signOut } from 'next-auth/client' -import * as gtag from '-utils/gtag' -import React from 'react' - -interface EditorProps { - id?: string -} - -export default function Editor({ id = 'home' }: EditorProps) { - // Put the state into the window, for debugging. - const handleMount = React.useCallback((state: TLDrawState) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window.state = state - }, []) - - // Send events to gtag as actions. - const handlePersist = React.useCallback((_state: TLDrawState, reason?: string) => { - gtag.event({ - action: reason, - category: 'editor', - label: reason || 'persist', - value: 0, - }) - }, []) - - const fileSystemEvents = useFileSystem() - - const handleSignIn = React.useCallback(() => { - signIn() - }, []) - - const handleSignOut = React.useCallback(() => { - signOut() - }, []) - - return ( -
    - -
    - ) -} diff --git a/www/hooks/useAccountHandlers.ts b/www/hooks/useAccountHandlers.ts new file mode 100644 index 000000000..3ceda9865 --- /dev/null +++ b/www/hooks/useAccountHandlers.ts @@ -0,0 +1,14 @@ +import * as React from 'react' +import { signIn, signOut } from 'next-auth/client' + +export function useAccountHandlers() { + const onSignIn = React.useCallback(() => { + signIn() + }, []) + + const onSignOut = React.useCallback(() => { + signOut() + }, []) + + return { onSignIn, onSignOut } +} diff --git a/www/hooks/useFileSystem.ts b/www/hooks/useFileSystem.ts deleted file mode 100644 index e3552914e..000000000 --- a/www/hooks/useFileSystem.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react' -import { TLDrawState } from '@tldraw/tldraw' - -export function useFileSystem() { - const promptSaveBeforeChange = React.useCallback(async (state: TLDrawState) => { - if (state.isDirty) { - if (state.fileSystemHandle) { - if (window.confirm('Do you want to save changes to your current project?')) { - await state.saveProject() - } - } else { - if (window.confirm('Do you want to save your current project?')) { - await state.saveProject() - } - } - } - }, []) - - const onNewProject = React.useCallback( - async (state: TLDrawState) => { - await promptSaveBeforeChange(state) - state.newProject() - }, - [promptSaveBeforeChange] - ) - - const onSaveProject = React.useCallback((state: TLDrawState) => { - state.saveProject() - }, []) - - const onSaveProjectAs = React.useCallback((state: TLDrawState) => { - state.saveProjectAs() - }, []) - - const onOpenProject = React.useCallback( - async (state: TLDrawState) => { - await promptSaveBeforeChange(state) - state.openProject() - }, - [promptSaveBeforeChange] - ) - - return { - onNewProject, - onSaveProject, - onSaveProjectAs, - onOpenProject, - } -} diff --git a/www/package.json b/www/package.json index f06fe6194..e0f2071e7 100644 --- a/www/package.json +++ b/www/package.json @@ -16,8 +16,8 @@ "lint": "next lint" }, "dependencies": { - "@liveblocks/client": "^0.12.0", - "@liveblocks/react": "^0.12.0", + "@liveblocks/client": "^0.12.3", + "@liveblocks/react": "^0.12.3", "@sentry/integrations": "^6.13.2", "@sentry/node": "^6.13.2", "@sentry/react": "^6.13.2", @@ -37,6 +37,7 @@ "@sentry/webpack-plugin": "^1.17.1", "@types/react": "^17.0.19", "@types/react-dom": "^17.0.9", + "cors": "^2.8.5", "eslint": "7.32.0", "eslint-config-next": "11.1.2", "typescript": "^4.4.2" diff --git a/www/pages/_app.tsx b/www/pages/_app.tsx index 3ef1a969f..10bb548e5 100644 --- a/www/pages/_app.tsx +++ b/www/pages/_app.tsx @@ -1,5 +1,6 @@ import '../styles/globals.css' import { init } from '-utils/sentry' +import Head from 'next/head' import useGtag from '-utils/useGtag' init() @@ -7,7 +8,18 @@ init() function MyApp({ Component, pageProps }) { useGtag() - return + return ( + <> + + + Tldraw + + + + ) } export default MyApp diff --git a/www/pages/_document.tsx b/www/pages/_document.tsx index c08bbdbf8..345913dca 100644 --- a/www/pages/_document.tsx +++ b/www/pages/_document.tsx @@ -2,7 +2,7 @@ import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'nex import { getCssText } from '../styles' import { GA_TRACKING_ID } from '../utils/gtag' -const APP_NAME = 'tldraw' +const APP_NAME = 'Tldraw' const APP_DESCRIPTION = 'A tiny little drawing app.' const APP_URL = 'https://tldraw.com' diff --git a/www/pages/index.tsx b/www/pages/index.tsx index 6e78baf15..f76e03a81 100644 --- a/www/pages/index.tsx +++ b/www/pages/index.tsx @@ -3,15 +3,20 @@ import { GetServerSideProps } from 'next' import { getSession } from 'next-auth/client' import Head from 'next/head' -const Editor = dynamic(() => import('components/editor'), { ssr: false }) +const Editor = dynamic(() => import('-components/Editor'), { ssr: false }) -export default function Shhh(): JSX.Element { +interface PageProps { + isUser: boolean + isSponsor: boolean +} + +export default function Home({ isUser, isSponsor }: PageProps): JSX.Element { return ( <> - tldraw + Tldraw - + ) } @@ -19,14 +24,10 @@ export default function Shhh(): JSX.Element { export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context) - if (!session?.user && process.env.NODE_ENV !== 'development') { - context.res.setHeader('Location', `/sponsorware`) - context.res.statusCode = 307 - } - return { props: { - session, + isUser: false, + isSponsor: session?.user ? true : false, }, } } diff --git a/www/pages/k/[id].tsx b/www/pages/k/[id].tsx deleted file mode 100644 index 03e5befd6..000000000 --- a/www/pages/k/[id].tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import type { GetServerSideProps } from 'next' -import Head from 'next/head' -import dynamic from 'next/dynamic' -const MultiplayerEditor = dynamic(() => import('components/multiplayer-editor'), { ssr: false }) - -interface RoomProps { - id: string -} - -export default function Room({ id }: RoomProps): JSX.Element { - return ( - <> - - tldraw - - - - ) -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - const id = context.query.id?.toString() - - return { - props: { - id, - }, - } -} diff --git a/www/pages/k/index.tsx b/www/pages/k/index.tsx deleted file mode 100644 index 73d12ee86..000000000 --- a/www/pages/k/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react' -import type { GetServerSideProps } from 'next' -import Head from 'next/head' - -interface RoomProps { - id?: string -} - -export default function RandomRoomPage({ id }: RoomProps): JSX.Element { - return ( - <> - - tldraw - -
    Should have routed to room: {id}
    - - ) -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - // Generate random id - const id = Date.now().toString() - - // Route to a room with that id - context.res.setHeader('Location', `/r/${id}`) - context.res.statusCode = 307 - - // Return id (though it shouldn't matter) - return { - props: { - id, - }, - } -} diff --git a/www/pages/r/[id].tsx b/www/pages/r/[id].tsx index 73b6f7afe..a228b21e7 100644 --- a/www/pages/r/[id].tsx +++ b/www/pages/r/[id].tsx @@ -1,39 +1,29 @@ import * as React from 'react' import type { GetServerSideProps } from 'next' -import Head from 'next/head' import { getSession } from 'next-auth/client' import dynamic from 'next/dynamic' -const MultiplayerEditor = dynamic(() => import('components/multiplayer-editor'), { ssr: false }) +const MultiplayerEditor = dynamic(() => import('-components/MultiplayerEditor'), { ssr: false }) interface RoomProps { id: string + isSponsor: boolean + isUser: boolean } -export default function Room({ id }: RoomProps): JSX.Element { - return ( - <> - - tldraw - - - - ) +export default function Room({ id, isUser, isSponsor }: RoomProps): JSX.Element { + return } export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context) - if (!session?.user && process.env.NODE_ENV !== 'development') { - context.res.setHeader('Location', `/sponsorware`) - context.res.statusCode = 307 - } - const id = context.query.id?.toString() return { props: { id, - session, + isUser: false, + isSponsor: session?.user ? true : false, }, } } diff --git a/www/pages/r/index.tsx b/www/pages/r/index.tsx index 73d12ee86..2af321c1d 100644 --- a/www/pages/r/index.tsx +++ b/www/pages/r/index.tsx @@ -10,7 +10,7 @@ export default function RandomRoomPage({ id }: RoomProps): JSX.Element { return ( <> - tldraw + Tldraw
    Should have routed to room: {id}
    diff --git a/www/pages/shhh.tsx b/www/pages/shhh.tsx deleted file mode 100644 index 4a50cbb4c..000000000 --- a/www/pages/shhh.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import dynamic from 'next/dynamic' -const Editor = dynamic(() => import('components/editor'), { ssr: false }) -import Head from 'next/head' - -export default function Shhh(): JSX.Element { - return ( - <> - - tldraw - - - - ) -} diff --git a/www/pages/shhhmp.tsx b/www/pages/shhhmp.tsx deleted file mode 100644 index 4ca1428e3..000000000 --- a/www/pages/shhhmp.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react' -import Head from 'next/head' -import dynamic from 'next/dynamic' -const MultiplayerEditor = dynamic(() => import('components/multiplayer-editor'), { ssr: false }) - -export default function Room(): JSX.Element { - return ( - <> - - tldraw - - - - ) -} diff --git a/www/pages/signout.tsx b/www/pages/signout.tsx deleted file mode 100644 index f237851cf..000000000 --- a/www/pages/signout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { signOut } from 'next-auth/client' -import Head from 'next/head' - -export default function SignOut(): JSX.Element { - return ( - <> - - tldraw - -
    - -
    - - ) -} diff --git a/www/pages/sponsorware.tsx b/www/pages/sponsorware.tsx index b2038940c..685f49455 100644 --- a/www/pages/sponsorware.tsx +++ b/www/pages/sponsorware.tsx @@ -11,7 +11,7 @@ export default function Sponsorware(): JSX.Element { return ( <> - tldraw + Tldraw -

    tldraw (is sponsorware)

    +

    Tldraw (is sponsorware)

    - Hey, thanks for visiting tldraw, a tiny little drawing app by{' '} + Hey, thanks for visiting Tldraw, a tiny little drawing app by{' '} - - tldraw - -

    - - ) -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - const session = await getSession(context) - - if (!session?.user && process.env.NODE_ENV !== 'development') { - context.res.setHeader('Location', `/sponsorware`) - context.res.statusCode = 307 - } - - const id = context.query.id?.toString() - - // Get user from database - - // If user does not exist, return null - - // Return the document - - return { - props: { - id, - session, - }, - } -} diff --git a/www/pages/u/index.tsx b/www/pages/u/index.tsx deleted file mode 100644 index d623c27c2..000000000 --- a/www/pages/u/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react' -import type { GetServerSideProps } from 'next' -import { getSession } from 'next-auth/client' -import type { Session } from 'next-auth' -import { signOut } from 'next-auth/client' -import Head from 'next/head' - -interface UserPageProps { - session: Session -} - -export default function UserPage({ session }: UserPageProps): JSX.Element { - return ( - <> - - tldraw - -
    -
    -          {JSON.stringify(session.user, null, 2)}
    -        
    - -
    - - ) -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - const session = await getSession(context) - - if (!session?.user && process.env.NODE_ENV !== 'development') { - context.res.setHeader('Location', `/sponsorware`) - context.res.statusCode = 307 - } - - const id = context.query.id?.toString() - - // Get document from database - - // If document does not exist, create an empty document - - // Return the document - - return { - props: { - id, - session, - }, - } -} diff --git a/www/styles/stitches.config.ts b/www/styles/stitches.config.ts index fb909e452..ae3200f1b 100644 --- a/www/styles/stitches.config.ts +++ b/www/styles/stitches.config.ts @@ -22,8 +22,8 @@ const { styled, globalCss, createTheme, getCssText } = createStitches({ inactive: '#cccccf', hover: '#efefef', text: '#333333', - tooltipBg: '#1d1d1d', - tooltipText: '#ffffff', + tooltip: '#1d1d1d', + tooltipContrast: '#ffffff', muted: '#777777', input: '#f3f3f3', inputBorder: '#dddddd', @@ -122,8 +122,8 @@ const dark = createTheme({ muted: '#e0e2e6', input: '#f3f3f3', inputBorder: '#ddd', - tooltipBg: '#1d1d1d', - tooltipText: '#ffffff', + tooltip: '#1d1d1d', + tooltipContrast: '#ffffff', codeHl: 'rgba(144, 144, 144, .15)', lineError: 'rgba(255, 0, 0, .1)', }, diff --git a/www/tsconfig.json b/www/tsconfig.json index ab5c2e3ee..e1328db22 100644 --- a/www/tsconfig.json +++ b/www/tsconfig.json @@ -23,10 +23,5 @@ "resolveJsonModule": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], - "references": [ - { - "path": "../packages/tldraw" - } - ] + "exclude": ["node_modules"] } diff --git a/www/types.ts b/www/types.ts new file mode 100644 index 000000000..ae3d978f6 --- /dev/null +++ b/www/types.ts @@ -0,0 +1,6 @@ +import { TDDocument } from '@tldraw/tldraw' +import { LiveObject } from '@liveblocks/client' + +export interface TldrawStorage { + doc: LiveObject<{ uuid: string; document: TDDocument }> +} diff --git a/yarn.lock b/yarn.lock index 2150e9530..c82739ad3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,12 +8,12 @@ integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ== "@apideck/better-ajv-errors@^0.2.4": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.2.6.tgz#f12e5176a04c84caade85100fa33317a1457f372" - integrity sha512-FvGcbFUdbPLexAhdvihkroCA3LQa7kGMa8Qj9f32BiOcV1Thscg/QCxp/kJibsFrhUrlKOzd07uJFOGTN0/awQ== + version "0.2.7" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.2.7.tgz#cc71652ecb111708c01bdc10206ca85886c118ea" + integrity sha512-J2dW+EHYudbwI7MGovcHWLBrxasl21uuroc2zT8bH2RxYuv2g5GqsO5jcKUZz4LaMST45xhKjVuyRYkhcWyMhA== dependencies: json-schema "^0.3.0" - jsonpointer "^4.1.0" + jsonpointer "^5.0.0" leven "^3.1.0" "@babel/code-frame@7.12.11": @@ -81,13 +81,13 @@ "@babel/types" "^7.16.0" "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.0.tgz#01d615762e796c17952c29e3ede9d6de07d235a8" - integrity sha512-S7iaOT1SYlqK0sQaCi21RX4+13hmdmnxIEAnQUB/eh7GeAnRjOUgTYpLkUOiRXzD+yog1JxP0qyAQZ7ZxVxLVg== + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" + integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== dependencies: "@babel/compat-data" "^7.16.0" "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.16.6" + browserslist "^4.17.5" semver "^6.3.0" "@babel/helper-create-class-features-plugin@^7.16.0": @@ -255,12 +255,12 @@ "@babel/types" "^7.16.0" "@babel/helpers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.0.tgz#875519c979c232f41adfbd43a3b0398c2e388183" - integrity sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ== + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" + integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== dependencies: "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" + "@babel/traverse" "^7.16.3" "@babel/types" "^7.16.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.16.0": @@ -272,10 +272,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.7.2": - version "7.16.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.2.tgz#3723cd5c8d8773eef96ce57ea1d9b7faaccd12ac" - integrity sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3", "@babel/parser@^7.7.2": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.3.tgz#271bafcb811080905a119222edbc17909c82261d" + integrity sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.0": version "7.16.2" @@ -717,9 +717,9 @@ "@babel/helper-replace-supers" "^7.16.0" "@babel/plugin-transform-parameters@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.0.tgz#1b50765fc421c229819dc4c7cdb8911660b3c2d7" - integrity sha512-XgnQEm1CevKROPx+udOi/8f8TiGhrUWiHiaUCIp47tE0tpFDjzXNTZc9E5CmCwxNjXTWEVqvRfWZYOTFvMa/ZQ== + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.3.tgz#fa9e4c874ee5223f891ee6fa8d737f4766d31d15" + integrity sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w== dependencies: "@babel/helper-plugin-utils" "^7.14.5" @@ -887,9 +887,9 @@ esutils "^2.0.2" "@babel/runtime-corejs3@^7.10.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.16.0.tgz#58a7fb00e6948508f12f53a303993e8b6e2f6c70" - integrity sha512-Oi2qwQ21X7/d9gn3WiwkDTJmq3TQtYNz89lRnoFy8VeZpWlsyXvzSwiRrRZ8cXluvSwqKxqHJ6dBd9Rv+p0ZGQ== + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.16.3.tgz#1e25de4fa994c57c18e5fdda6cc810dac70f5590" + integrity sha512-IAdDC7T0+wEB4y2gbIL0uOXEYpiZEeuFUTVbdGq+UwCcF35T/tS8KrmMomEwEc5wBbyfH3PJVpTSUqrhPDXFcQ== dependencies: core-js-pure "^3.19.0" regenerator-runtime "^0.13.4" @@ -901,10 +901,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.0.tgz#e27b977f2e2088ba24748bf99b5e1dece64e4f0b" - integrity sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== dependencies: regenerator-runtime "^0.13.4" @@ -917,17 +917,17 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.7.2": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.0.tgz#965df6c6bfc0a958c1e739284d3c9fa4a6e3c45b" - integrity sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3", "@babel/traverse@^7.7.2": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" + integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== dependencies: "@babel/code-frame" "^7.16.0" "@babel/generator" "^7.16.0" "@babel/helper-function-name" "^7.16.0" "@babel/helper-hoist-variables" "^7.16.0" "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.0" + "@babel/parser" "^7.16.3" "@babel/types" "^7.16.0" debug "^4.1.0" globals "^11.1.0" @@ -1999,12 +1999,12 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" -"@liveblocks/client@^0.12.0", "@liveblocks/client@^0.12.1": +"@liveblocks/client@^0.12.3": version "0.12.3" resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.12.3.tgz#03b957ccc7a6a5dc7474d224fe12c32e065e9c9c" integrity sha512-n82Ymngpvt4EiZEU3LWnEq7EjDmcd2wb2kjGz4m/4L7wYEd4RygAYi7bp7w5JOD1rt3Srhrwbq9Rz7TikbUheg== -"@liveblocks/react@^0.12.0", "@liveblocks/react@^0.12.1": +"@liveblocks/react@^0.12.3": version "0.12.3" resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.12.3.tgz#82d93a9a3a96401258f6c87c1150026dd9d63504" integrity sha512-3mHRiEwZ/s1lbGS4/bblUpLCNCBFMzEiUHHfBH3zO9+IKrH40lVdky0OujgF5zEacYcqUnVW7jT4ZvHCchvsYA== @@ -2806,14 +2806,14 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== -"@sentry/browser@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.14.1.tgz#4d255caf9de6e07f12b6d9b350fe391439dd932e" - integrity sha512-xOrKt6jT6rGhJDVwUtHtD/lLrCOEDNYCtLAh8SoJH7jE0JRSI7WK0UDPQ56M8z3II11lEw3F0TOXoK1rZ9BdrQ== +"@sentry/browser@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.14.3.tgz#4e3b67a48b12a70c381cab326d053ee5dfc087d6" + integrity sha512-qp4K+XNYNWQxO1U6gvf6VgOMmI0JKCsvx1pKu7X4ZK7sGHmMgfwj7lukpxsqXZvDop8RxUI8/1KJ0azUsHlpAQ== dependencies: - "@sentry/core" "6.14.1" - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/core" "6.14.3" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" tslib "^1.9.3" "@sentry/cli@^1.70.1": @@ -2828,94 +2828,94 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/core@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.14.1.tgz#cbb6eae808279ae2147dd5da22ce6ab5a1cd69d1" - integrity sha512-x2MOax+adphal0ytBsvQukwN5mcxZzb5zsPZ1YWzewQk3BY+2T/DFo50iVpaWdUXsJL2FtoZVVgtpTmf+/3JPw== +"@sentry/core@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.14.3.tgz#42d255c1a8838e8f9d122b823ba5ff5c27803537" + integrity sha512-3yHmYZzkXlOqPi/CGlNhb2RzXFvYAryBhrMJV26KJ9ULJF8r4OJ7TcWlupDooGk6Knmq8GQML58OApUvYi8IKg== dependencies: - "@sentry/hub" "6.14.1" - "@sentry/minimal" "6.14.1" - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/hub" "6.14.3" + "@sentry/minimal" "6.14.3" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" tslib "^1.9.3" -"@sentry/hub@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.14.1.tgz#6a82cae35de834bd92bbcd3912a1e3029a5369de" - integrity sha512-IqANj5qKG1N+nqBsuYIwAZsXDMmO/Sc4H2zZ2MP7QvRyp0ptpJmu1oTE0r0fohIcGgIWbnIphJjw990Lp507eA== +"@sentry/hub@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.14.3.tgz#f6e84e561a4aff1a4447927356fea541465364c1" + integrity sha512-ZRWLHcAcv4oZAbpSwvCkXlaa1rVFDxcb9lxo5/5v5n6qJq2IG5Z+bXuT2DZlIHQmuCuqRnFSwuBjmBXY7OTHaw== dependencies: - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" tslib "^1.9.3" "@sentry/integrations@^6.13.2": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-6.14.1.tgz#2293d954cdb14e1992f6bdc6484275defb2667f2" - integrity sha512-GZFp03w0c/o2iY1oxebsqpumsDQsoOWiUXpfPriG6UoXZdCDrSoiotaRHPAagsWKQ1XjbILph8Vdz5dSMIuQTg== + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-6.14.3.tgz#4b32ce1e52d0abff94befe5e52d3ee4327211322" + integrity sha512-TGdf0l6GzPCEUxkIgU3bgG9DfFni3L97r/BPzwhc0izvkZ+O+3UWu6nCVHp1yWEZrskIxLszDhRz2N5D1zldPQ== dependencies: - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" localforage "^1.8.1" tslib "^1.9.3" -"@sentry/minimal@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.14.1.tgz#6fbce5b873fb096411dbb9a01ff6706ed684f2e8" - integrity sha512-rxS0YUggCSuA7EzS1ai5jU8XArk4FBHZ02gmSoSSLtwFXmeQIa9XBKY0OEFmG2LMQYNOpvcGsezDO51EB6/X9w== +"@sentry/minimal@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.14.3.tgz#f3a5b062bdc578000689fd0b31abbb994e6b81f3" + integrity sha512-2KNOJuhBpMICoOgdxX56UcO9vGdxCw5mNGYdWvJdKrMwRQr7mC+Fc9lTuTbrYTj6zkfklj2lbdDc3j44Rg787A== dependencies: - "@sentry/hub" "6.14.1" - "@sentry/types" "6.14.1" + "@sentry/hub" "6.14.3" + "@sentry/types" "6.14.3" tslib "^1.9.3" "@sentry/node@^6.13.2": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.14.1.tgz#475229eb0a3c7032a905a7f49888e1374499ecf3" - integrity sha512-tnEfcaF5Z7I4D619XL76sjRd7VMDitZZ7ydfA8sWGC1BPaPyyIJzVxE/a7qJBQGW7W0Oo7ctwOI1hpmfyOpPxg== + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.14.3.tgz#f19f22f6b73242c0dbda204f8da2e72e38067b65" + integrity sha512-b7NjMdqpDOTxV0hiR90jlK52i9cTdAJgGjQykGFyBDf7rTGDohyEYsERgJ5+/VC3Inan/P3m12PctWA/TMwZCw== dependencies: - "@sentry/core" "6.14.1" - "@sentry/hub" "6.14.1" - "@sentry/tracing" "6.14.1" - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/core" "6.14.3" + "@sentry/hub" "6.14.3" + "@sentry/tracing" "6.14.3" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" cookie "^0.4.1" https-proxy-agent "^5.0.0" lru_map "^0.3.3" tslib "^1.9.3" "@sentry/react@^6.13.2": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.14.1.tgz#26889c2c6d61a1d9ffa2f82c72438e7c3ad8bdc7" - integrity sha512-A+GEb0g8EW3JmTRSAEws2Sx9QIldHuDW3P6R45Qq6T/g6nzxUtAa6gVdmGt40JwfHofzQgQDRca4baqtrHDsHw== + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.14.3.tgz#b0fec4266d851d703fc21e79c1290bd77892d356" + integrity sha512-kHadqr7o2CmqYWByXWNlPZRn30K0HzlkODvML21ztRz4QPZVq/6jvTbFhfdTz6rKa2J/bBgcIE1101Ie5ZErOg== dependencies: - "@sentry/browser" "6.14.1" - "@sentry/minimal" "6.14.1" - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/browser" "6.14.3" + "@sentry/minimal" "6.14.3" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" hoist-non-react-statics "^3.3.2" tslib "^1.9.3" -"@sentry/tracing@6.14.1", "@sentry/tracing@^6.13.2": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.14.1.tgz#fadea88b505078f61b949ecd99891ddb5538f08e" - integrity sha512-Bv/+S5Wn9OPxP7sA9VYMV1wpmXWptFVIMFoG4BuyV4aFYdIAMxSNE/ktqXwmqn+nkBic04nP9rF6lMJBLIvIaA== +"@sentry/tracing@6.14.3", "@sentry/tracing@^6.13.2": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.14.3.tgz#0223d365ea0c7d3f7c90cb17ea84c4874bc9ef52" + integrity sha512-laFayAxpO/dQL3K3ZcSjtaqJkSf70DH1hHJ8Oiiic0c/xBxh38WSx8yu3TMrbfka5MVIuMNlkq1Gi+SC+moe4w== dependencies: - "@sentry/hub" "6.14.1" - "@sentry/minimal" "6.14.1" - "@sentry/types" "6.14.1" - "@sentry/utils" "6.14.1" + "@sentry/hub" "6.14.3" + "@sentry/minimal" "6.14.3" + "@sentry/types" "6.14.3" + "@sentry/utils" "6.14.3" tslib "^1.9.3" -"@sentry/types@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.14.1.tgz#0d562a7aa91253b7843723344b4ba03a010e6376" - integrity sha512-RIk3ZwQKZnASrYWfV5i4wbzVveHz8xLFAS2ySIMqh+hICKnB0N4/r8a1Of/84j7pj+iAbf5vPS85639eIf+9qg== +"@sentry/types@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.14.3.tgz#4af799df7ddfa2702a46bffabc3f1b6eb195de23" + integrity sha512-GuyqvjQ/N0hIgAjGD1Rn0aQ8kpLBBsImk+Aoh7YFhnvXRhCNkp9N8BuXTfC/uMdMshcWa1OFik/udyjdQM3EJA== -"@sentry/utils@6.14.1": - version "6.14.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.14.1.tgz#cb746858665314c07cfe9b0f307b410e377032ad" - integrity sha512-GVvf0z18L4DN0a6vIBdHSlrK/Dj8QFhuiiJ8NtccSoY8xiKXQNz9FKN5d52NUNqm59aopAxcVAcs57yQSdxrZQ== +"@sentry/utils@6.14.3": + version "6.14.3" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.14.3.tgz#4ae907054152882fbd376906695ac326934669d1" + integrity sha512-jsCnclEsR2sV9aHMuaLA5gvxSa0xV4Sc6IJCJ81NTTdb/A5fFbteFBbhuISGF9YoFW1pwbpjuTA6+efXwvLwNQ== dependencies: - "@sentry/types" "6.14.1" + "@sentry/types" "6.14.3" tslib "^1.9.3" "@sentry/webpack-plugin@^1.17.1": @@ -2979,100 +2979,100 @@ ejs "^2.6.1" magic-string "^0.25.0" -"@swc-node/core@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.7.1.tgz#05e45051ccd975b25ddd0cf62db477e6a2e4ee4b" - integrity sha512-m/21f7irvu+/P3Lqz2qEJVUf10vqqMp/4sUtxxn7tVjqKwTWPJF7DbZGsaEcWXLC9UxFdw7hLrNY61OL7hJNSA== +"@swc-node/core@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.8.0.tgz#76389ab2881bd9caa5ccf90932856455271c53da" + integrity sha512-oolF9LG4GP6NhUMWqGi2bDomE3v0CYmNl/kJN2+Hh+iYLdv7l36B0GWFGLnSnq0sWyjrp+1Ur7MZTZFGnK1a3w== dependencies: "@swc/core" "^1.2.104" "@swc-node/jest@^1.3.3": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@swc-node/jest/-/jest-1.3.5.tgz#68640603196e75f6b437460fb92abb4794f2c154" - integrity sha512-TwwLa1b+L6lPGrrrNlZsnaNsmUt5NmOuItJ/DVWn8FqwfjpxZ0TORA3dUc/6oONAsRmCKf02u9UhHd0x3CMiUQ== + version "1.4.0" + resolved "https://registry.yarnpkg.com/@swc-node/jest/-/jest-1.4.0.tgz#efecb67b68109c8f5c7d73190e678141391d54d4" + integrity sha512-fPdv7L/EFQ0VHQCBO9WC7NrzEZsVQrO3OZW5pX4W2ahLOfRvjd8dw/QKIREuvwWtuvfFFg+knRXKqj88Qq8mHg== dependencies: "@node-rs/xxhash" "^1.0.0" - "@swc-node/core" "^1.7.1" + "@swc-node/core" "^1.8.0" -"@swc/core-android-arm64@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.107.tgz#b9dfd19b9b1659e7dc47d2a1fe4578e56b5c01f1" - integrity sha512-gnMkRn6DPDFiPcH1VC15XsQzR1/9SW0CqwYUiBUEuS5wZbOnyEkgY3UChu8SgeMDbzDx5KJMrVjed1UrMZU26w== +"@swc/core-android-arm64@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.108.tgz#8d0099308ceb97967ac80f00892bc6b1cfd1cb5e" + integrity sha512-/+8hIUYptTpLi6gyIJuWyO8+nR661q3nlJdEBfmG9hzVEq7vK6wQQG9ctYfI8eRThnWXwg95O1hwQQRN7fgnRQ== -"@swc/core-darwin-arm64@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.107.tgz#c6e2a9372728a3ce7f3daf33169809940c39577c" - integrity sha512-N1NG6SHAyJqhkPzMj2+jBbeY4jgS/ShIY8s1GyvRKKSjgqjBKiZvNwgFzWZ7lf16kTJO4rSG//NnPr8noL19yw== +"@swc/core-darwin-arm64@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.108.tgz#42541173f5bc53d8b632bae9143ec70cbea1b327" + integrity sha512-f39PTTnRHnX9I+XtvbwTbyMBF+JbzkJMrzNE4i2PS8dYSC5yVHI3S5CEqh0pG7Ihu+V1vmkPDs+qN7WgJPQ/2w== -"@swc/core-darwin-x64@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.107.tgz#8fab5165123792d7a7d7289a6c8d251269fc00cf" - integrity sha512-cYA4YsrUtOTHMWnKUk/X3l5UTdpOt90SExg+v7hSonhJSg84yBoXOwNzfPVcsP5Af5rWLABrLxpOOQKNCWNf6g== +"@swc/core-darwin-x64@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.108.tgz#1e675da3012ac6e3a28b23e029c0366a5f8c0c21" + integrity sha512-DGSgGj5hdZWQdTY9neUXcmH48bH4dF2AHPtDh7Ce49HnETn2oztf/4Fhfnpa3z97jrhvxZ9WvE0Mvrvu1wAmBw== -"@swc/core-freebsd-x64@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.107.tgz#ea5b6c1c29d4905ade00a11cd9344e0a70be73d2" - integrity sha512-G4RPAZnrBIAoUoAddpbPqeM6CM71m1PM/Y6mXA4iriRo0ro74Og8hlJ9IjsxWM3YXrTgXH6xZ9F0iewhtaKpYA== +"@swc/core-freebsd-x64@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.108.tgz#6c32cf6935194de3792e899fe4225ac0e0090fcc" + integrity sha512-fuynErhCBhc6JABGExU0bp/BAsmIWueila8HeWFkNBFH0SoVfnKkRGLSfGRPn/nFvvjniBxYYEwUEqewKrpS3w== -"@swc/core-linux-arm-gnueabihf@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.107.tgz#f782ff6dd99e405826bd7e192a6e51f15f2070e0" - integrity sha512-7fAK/jSQAnZ9qtZvxwaoCcegT4BDDEyOIulm+fBVZCAcQb/2zZwZuG2P7t2Pzfj1ftEy2A8YPKaUhHl6llOhUg== +"@swc/core-linux-arm-gnueabihf@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.108.tgz#ea6e2d44b64fe19c83ffafa46a4ac9beb24e33d5" + integrity sha512-90X5xQqpU6EBNSMdjjAV+VhJBw4HS4H01zpFvMSgcOYDUTa3lZf5L5m1KBDOUg3XczotUtP6JJPVIR07GpR82g== -"@swc/core-linux-arm64-gnu@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.107.tgz#0b63e2330604fe07f18aa0e1842dd1c498514b8e" - integrity sha512-E0l2hhlsTzl70OqBKqcm8+8rz6zYdNAtce8FM8vmezvgKgIfqlONz2tQyHNkkSKytV6uL5gjla9Ot+aLk2DrLw== +"@swc/core-linux-arm64-gnu@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.108.tgz#72bc35f44bf66a4dcaca0e7c409b8a8e5858f0d6" + integrity sha512-sEIckU72jDgoC3yzbqvW08Uwy8tenvJwygh+2CiEGHJ/YvaXA+wrrBSYp/HStvYMZXOJP6e1FVAELTkDNJbkjg== -"@swc/core-linux-arm64-musl@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.107.tgz#8451f84867c17e6cc79259f40438481c5951dc47" - integrity sha512-mdmZ34H3tolvIfjeoFPSxy4AqyM4NucNAVDPU6vRf9imlPcMiI6mFhqwwI0pa4edYVB0pSU34z6Te5LpcrbhrQ== +"@swc/core-linux-arm64-musl@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.108.tgz#18277019b3ce1d0d4d39dff892835e9469d971a9" + integrity sha512-jjdojRe2MdkiI4/cwjn6nzOoUhamRYJYFT4RH9CeVWou43iAQvxAX+f0nLo1GKrpmzQ9knp969iO0bgCB0Ax6A== -"@swc/core-linux-x64-gnu@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.107.tgz#7381920c40021c3aecd999e61bcfad4b3580102e" - integrity sha512-zY80CTn5h35pHJw+cg2WbAhBICdbzHtEU4o3DJKkx1y26gk3XjvLnEUSsot+eTezthzQyaPuiN1DsHEX1kSouQ== +"@swc/core-linux-x64-gnu@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.108.tgz#e7436ac83988b725603a8d5391b9c02aefca80e7" + integrity sha512-SteNOLrIp6YtaQkHzzmvVj4Xu3usvDM7R97Qf8bvnsZ7GyIotfVkIGbf6PttGtMKDNMxNnrsY1/6Z/zDcOtiKw== -"@swc/core-linux-x64-musl@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.107.tgz#a8ca63c3ebca01d02e1dd1ec10b7dbea48fdefa7" - integrity sha512-CIs9oh6QsAiIiZyAS47WcpHklXonNBQ6dg7NXKXXsz9tpAsYqfjs/RWQSH8O6cPihfj0JR2KdfTVyIzXhKfOjg== +"@swc/core-linux-x64-musl@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.108.tgz#3f19b48e34c620507efa2c817e60f7c3638d6034" + integrity sha512-vOlBtLsi+mchJfTIMZmf3Lif6PAXFPfEFXcZRBFC6M9GxFgF1hgscVvSmdcyLpKqMN8a25fOKbdA6qcqQ3qrwg== -"@swc/core-win32-arm64-msvc@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.107.tgz#03abc92592edbed72e510d04415297a8a3ae14bf" - integrity sha512-5fJTruURSwpLYjoEpc/ZM8LZHB5zbChbEuZn5+Nb5EnxM5vsgHJe6+ZozQ3rpN7BS46nvlWiz14AeuLBsHIH6w== +"@swc/core-win32-arm64-msvc@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.108.tgz#6d293a12a87fed5063d62061c7812b47a2a6ebab" + integrity sha512-Rz1/jqJgTysMt2l3gT6/pIeVdQgsIQcK4sKYTD3VttvZ4L+eNv3LGubkrDWPGz4RWq/fZn2aRoQTp09POrV2RA== -"@swc/core-win32-ia32-msvc@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.107.tgz#38c66fe44e3b78ba3b8855874e48e258219050e4" - integrity sha512-mmgdLtv72Axa/fEkXcw/cv5FkNWzxz9wv+6cy2FQy9xDeY8hTD/GBDdgIolkbFfDiY+NS1N7dUzYArsxUJLBow== +"@swc/core-win32-ia32-msvc@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.108.tgz#b4733040cc8f2eb830e096694ffe8d229df4faa4" + integrity sha512-Mr/WPgBt8IjhJkukT3Img+pzC6SAlp5c0iBjO/Vy+uBHLz3b1/gutbc1FtqyFfbjqtmygIq0wir1m7gXHtfMhA== -"@swc/core-win32-x64-msvc@^1.2.107": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.107.tgz#fe42bdc3be99de56f289fb34b9e0b35edca8345e" - integrity sha512-W13K5ezQRGBYIgVIy8SIdnoAFWqLX6dYa3KN/Ox75usej+tukP42+CdRJloE/wsdIb12xiKkTU3fpNodJOe2+A== +"@swc/core-win32-x64-msvc@^1.2.108": + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.108.tgz#fe5aba7d09db0bf7dff6bf5c9a5c69a2b4395cf9" + integrity sha512-wH7+u1DUwHZOwR7bbX1hmXmHpvylxxnwo/Nh4X0kx9aRCBMvDnNHqnQb5jl1FysEpVYzntLvjX2GSYLHQdJ/Ag== "@swc/core@^1.2.104": - version "1.2.107" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.107.tgz#e269cad35cc03d39016d7747fc3f2f7e3691b92e" - integrity sha512-tkXwcDHdcC8cTaeH5ezpAK3BwDk6H7jmo5/+zsbMJiJgHjQGUGf+81whXIE9iwUUBVISi75FP/VHPEC+qNtg+Q== + version "1.2.108" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.108.tgz#f88e854e17473b9990a70b1ad350e815788f87ef" + integrity sha512-OnENqiVLs1a9fy8lnEh3EtLgmFRgK0fZDCTV8CxVpMIzrYr7KnAZ/H4hc6MnxMkxqc+EJJWDIAd6LQ2ZHvLmNg== dependencies: "@node-rs/helper" "^1.0.0" optionalDependencies: - "@swc/core-android-arm64" "^1.2.107" - "@swc/core-darwin-arm64" "^1.2.107" - "@swc/core-darwin-x64" "^1.2.107" - "@swc/core-freebsd-x64" "^1.2.107" - "@swc/core-linux-arm-gnueabihf" "^1.2.107" - "@swc/core-linux-arm64-gnu" "^1.2.107" - "@swc/core-linux-arm64-musl" "^1.2.107" - "@swc/core-linux-x64-gnu" "^1.2.107" - "@swc/core-linux-x64-musl" "^1.2.107" - "@swc/core-win32-arm64-msvc" "^1.2.107" - "@swc/core-win32-ia32-msvc" "^1.2.107" - "@swc/core-win32-x64-msvc" "^1.2.107" + "@swc/core-android-arm64" "^1.2.108" + "@swc/core-darwin-arm64" "^1.2.108" + "@swc/core-darwin-x64" "^1.2.108" + "@swc/core-freebsd-x64" "^1.2.108" + "@swc/core-linux-arm-gnueabihf" "^1.2.108" + "@swc/core-linux-arm64-gnu" "^1.2.108" + "@swc/core-linux-arm64-musl" "^1.2.108" + "@swc/core-linux-x64-gnu" "^1.2.108" + "@swc/core-linux-x64-musl" "^1.2.108" + "@swc/core-win32-arm64-msvc" "^1.2.108" + "@swc/core-win32-ia32-msvc" "^1.2.108" + "@swc/core-win32-x64-msvc" "^1.2.108" "@szmarczak/http-timer@^1.1.2": version "1.1.2" @@ -3082,9 +3082,9 @@ defer-to-connect "^1.0.1" "@testing-library/dom@^8.0.0": - version "8.11.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.0.tgz#3679dfb4db58e0d2b95e4b0929eaf45237b60d94" - integrity sha512-8Ay4UDiMlB5YWy+ZvCeRyFFofs53ebxrWnOFvCoM1HpMAX4cHyuSrCuIM9l2lVuUWUt+Gr3loz/nCwdrnG6ShQ== + version "8.11.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.1.tgz#03fa2684aa09ade589b460db46b4c7be9fc69753" + integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" @@ -3119,18 +3119,18 @@ "@testing-library/dom" "^8.0.0" "@tldraw/core@latest": - version "0.1.19" - resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.1.19.tgz#4bed3b8faeb48933aa75761cf76665bd69d0abe6" - integrity sha512-O/ZHfd6qEg8G+0rEIcd3trEhbF9R2SdEG9H0BZUo2ZG+6JyJNUDka2OCJi6gdFg90JEvLCq5rKg23ay7qEUidQ== + version "0.1.21" + resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.1.21.tgz#43727501f5720da93c10598f979d5ec0e8029b89" + integrity sha512-qaqXqVbeEq9jbjBsODS+UV/7opf4TyLOAi58gw94XC6tPwHpl8pkIvCKnyl10lHhac0GxGxSB+MPyKhlKioeZg== dependencies: "@tldraw/intersect" latest "@tldraw/vec" latest "@use-gesture/react" "^10.1.3" "@tldraw/intersect@latest": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@tldraw/intersect/-/intersect-0.1.3.tgz#ea784576632084710cebab0ff0a56de9d2cb6fa4" - integrity sha512-8NeLOuVmiqhHTBeqFuMP3ljS8vUirenMteX336O+/2H4IfT3FiJjeN3qZyTT5N5jZvaHJKraHOp4E45wraoNZQ== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@tldraw/intersect/-/intersect-0.1.4.tgz#390d76a0a24f625dfb9bb08f7f6a24e4a02007a1" + integrity sha512-pxNMyUD9BMy0JqUSik4RXhf/meRA1TwJ3zESLebQ3EB3kThd9zICIUH5U8RmosvBx0c7dLMTvVzA1yVRbQBslQ== dependencies: "@tldraw/vec" "^0.1.3" @@ -3281,14 +3281,14 @@ next-auth "*" "@types/node@*", "@types/node@>= 8": - version "16.11.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== + version "16.11.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42" + integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== "@types/node@^14.14.35", "@types/node@^14.6.2": - version "14.17.32" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" - integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== + version "14.17.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.33.tgz#011ee28e38dc7aee1be032ceadf6332a0ab15b12" + integrity sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g== "@types/node@^15.0.1": version "15.14.9" @@ -3314,9 +3314,9 @@ xmlbuilder ">=11.0.1" "@types/prettier@^2.1.5": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" - integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" + integrity sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA== "@types/prop-types@*": version "15.7.4" @@ -3517,17 +3517,17 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@use-gesture/core@10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.1.3.tgz#3fc3515af7b1bf26e2035350d653e3287a6e1fe0" - integrity sha512-eAwlcE7vppFtaki6mcrSwRa88Z2JAPlfNh35b1MRzhhutgKCUmudYur5JfrKPEvjnUk+KGXWFHCHVfb63c8DPQ== +"@use-gesture/core@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.1.4.tgz#72a199eafcd1ef97eebab96b11ae0b2559bf7eba" + integrity sha512-7TcntiiON64kGPDovAbjwh2C8+qL2Ts8RaRFNTsWiWv7nOAGbvT969JHJ0zRyJYg8PlAXeCf/q0IpoMyou731Q== "@use-gesture/react@^10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.1.3.tgz#a884f8a345a2a602a1aac42b11746f9e48fdc10f" - integrity sha512-i7q2k9pRmNG3YWiMMKSQd99uZQoo20OCwT238ejkWdWru8+0SWJmnDdXA4UfYLT47G4ASEpbMtkTkduDVAm1WA== + version "10.1.4" + resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.1.4.tgz#d6b6271bef8043bb444289283b9215966e0c09ed" + integrity sha512-1CoMHpN2b2ALqZEcfnxIx+MibVhxx+6u4gGOxwRtkr/emFYJA+bWFtWSYdOM4r8ALGT+1yeVUJJg1YKHDnL8EA== dependencies: - "@use-gesture/core" "10.1.3" + "@use-gesture/core" "10.1.4" "@vscode/test-web@^0.0.12": version "0.0.12" @@ -3662,9 +3662,9 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.1, ajv@^8.6.0: - version "8.6.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" - integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== + version "8.8.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.0.tgz#c501f10df72914bb77a458919e79fc73e4a2f9ef" + integrity sha512-L+cJ/+pkdICMueKR6wIx3VP2fjIx3yAhuvadUv/osv9yFD7OVZy442xFF+Oeu3ZvmhBGQzoF6mTSt+LUWBmGQg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -3720,11 +3720,6 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -3882,7 +3877,7 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.1.1, array-includes@^3.1.3, array-includes@^3.1.4: +array-includes@^3.1.3, array-includes@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== @@ -3924,7 +3919,7 @@ array.prototype.flat@^1.2.5: define-properties "^1.1.3" es-abstract "^1.19.0" -array.prototype.flatmap@^1.2.4: +array.prototype.flatmap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== @@ -4052,7 +4047,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axe-core@^4.0.2: +axe-core@^4.3.5: version "4.3.5" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== @@ -4410,13 +4405,13 @@ browserslist@4.16.6: escalade "^3.1.1" node-releases "^1.1.71" -browserslist@^4.16.6, browserslist@^4.17.6: - version "4.17.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.6.tgz#c76be33e7786b497f66cad25a73756c8b938985d" - integrity sha512-uPgz3vyRTlEiCv4ee9KlsKgo2V6qPk7Jsn0KAn2OBqbqKo3iNcPEC1Ti6J4dwnz+aIRfEEEuOzC9IBk8tXUomw== +browserslist@^4.17.5, browserslist@^4.17.6: + version "4.18.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.0.tgz#849944d9bbbbe5ff6f418a8b558e3effca433cae" + integrity sha512-ER2M0g5iAR84fS/zjBDqEgU6iO5fS9JI2EkHr5zxDxYEFk3LjhU9Vpp/INb6RMQphxko7PDV1FH38H/qVP5yCA== dependencies: - caniuse-lite "^1.0.30001274" - electron-to-chromium "^1.3.886" + caniuse-lite "^1.0.30001280" + electron-to-chromium "^1.3.896" escalade "^3.1.1" node-releases "^2.0.1" picocolors "^1.0.0" @@ -4704,10 +4699,10 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001274: - version "1.0.30001278" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001278.tgz#51cafc858df77d966b17f59b5839250b24417fff" - integrity sha512-mpF9KeH8u5cMoEmIic/cr7PNS+F5LWBk0t2ekGT60lFf0Wq+n9LspAj0g3P+o7DQhD3sUdlMln4YFAWhFYn9jg== +caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001280: + version "1.0.30001280" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7" + integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA== caseless@~0.12.0: version "0.12.0" @@ -4731,17 +4726,6 @@ chalk@4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -5088,9 +5072,9 @@ commander@^8.2.0: integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + version "1.8.1" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.1.tgz#ebf4973edf7d476a9c19646f5f04a45f95796029" + integrity sha512-uOZd85rJqrdEIE/JjhW5YAeatX8iqjjvVzIyfx7JL7G5r9Tep6YpYT9gEJWhWpVyDQEyzukWd6p2qULpJ8tmBw== commondir@^1.0.1: version "1.0.1" @@ -5381,6 +5365,14 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -5565,7 +5557,7 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -damerau-levenshtein@^1.0.6: +damerau-levenshtein@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== @@ -5922,9 +5914,9 @@ detective-cjs@^3.1.1: node-source-walk "^4.0.0" detective-es6@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-2.2.0.tgz#8f2baba3f8cd90a5cfd748f5ac436f0158ed2585" - integrity sha512-fSpNY0SLER7/sVgQZ1NxJPwmc9uCTzNgdkQDhAaj8NPYwr7Qji9QBcmbNvtMCnuuOGMuKn3O7jv0An+/WRWJZQ== + version "2.2.1" + resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-2.2.1.tgz#090c874e2cdcda677389cc2ae36f0b37faced187" + integrity sha512-22z7MblxkhsIQGuALeGwCKEfqNy4WmgDGmfJCwdXbfDkVYIiIDmY513hiIWBvX3kCmzvvWE7RR7kAYxs01wwKQ== dependencies: node-source-walk "^4.0.0" @@ -6287,10 +6279,10 @@ electron-publish@22.13.1: lazy-val "^1.0.5" mime "^2.5.2" -electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.886: - version "1.3.890" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.890.tgz#e7143b659f73dc4d0512d1ae4baeb0fb9e7bc835" - integrity sha512-VWlVXSkv0cA/OOehrEyqjUTHwV8YXCPTfPvbtoeU2aHR21vI4Ejh5aC4AxUwOmbLbBgb6Gd3URZahoCxtBqCYQ== +electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.896: + version "1.3.896" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz#4a94efe4870b1687eafd5c378198a49da06e8a1b" + integrity sha512-NcGkBVXePiuUrPLV8IxP43n1EOtdg+dudVjrfVEUd/bOqpQUFZ2diL5PPYzbgEhZFEltdXV3AcyKwGnEQ5lhMA== electron-util@^0.17.2: version "0.17.2" @@ -6337,7 +6329,7 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.0.0: +emoji-regex@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== @@ -6475,20 +6467,20 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild-android-arm64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz#e1f199dc05405cdc6670c00fb6c793822bf8ae4c" - integrity sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw== +esbuild-android-arm64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz#da07b5fb2daf7d83dcd725f7cf58a6758e6e702a" + integrity sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ== -esbuild-darwin-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz#f5c59e622955c01f050e5a7ac9c1d41db714b94d" - integrity sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw== +esbuild-darwin-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz#e94e9fd3b4b5455a2e675cd084a19a71b6904bbf" + integrity sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA== -esbuild-darwin-arm64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz#8abae74c2956a8aa568fc52c78829338c4a4b988" - integrity sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ== +esbuild-darwin-arm64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz#8c320eafbb3ba2c70d8062128c5b71503e342471" + integrity sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g== esbuild-envfile-plugin@^1.0.1: version "1.0.1" @@ -6497,55 +6489,55 @@ esbuild-envfile-plugin@^1.0.1: dependencies: dotenv "8.2.0" -esbuild-freebsd-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz#6ad2ab8c0364ee7dd2d6e324d876a8e60ae75d12" - integrity sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA== +esbuild-freebsd-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz#ce0ca5b8c4c274cfebc9326f9b316834bd9dd151" + integrity sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A== -esbuild-freebsd-arm64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz#6f38155f4c300ac4c8adde1fde3cc6a4440a8294" - integrity sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw== +esbuild-freebsd-arm64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz#463da17562fdcfdf03b3b94b28497d8d8dcc8f62" + integrity sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw== -esbuild-linux-32@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz#b1d15e330188a8c21de75c3f0058628a3eefade7" - integrity sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ== +esbuild-linux-32@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz#2035793160da2c4be48a929e5bafb14a31789acc" + integrity sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w== -esbuild-linux-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz#25bd64b66162b02348e32d8f12e4c9ee61f1d070" - integrity sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q== +esbuild-linux-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz#fbe4802a8168c6d339d0749f977b099449b56f22" + integrity sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw== -esbuild-linux-arm64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz#ba582298457cc5c9ac823a275de117620c06537f" - integrity sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA== +esbuild-linux-arm64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz#f08d98df28d436ed4aad1529615822bb74d4d978" + integrity sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng== -esbuild-linux-arm@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz#6bc81c957bff22725688cc6359c29a25765be09b" - integrity sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw== +esbuild-linux-arm@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz#6f968c3a98b64e30c80b212384192d0cfcb32e7f" + integrity sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ== -esbuild-linux-mips64le@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz#ef3c4aba3e585d847cbade5945a8b4a5c62c7ce2" - integrity sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g== +esbuild-linux-mips64le@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz#690c78dc4725efe7d06a1431287966fbf7774c7f" + integrity sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w== -esbuild-linux-ppc64le@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz#a21fb64e80c38bef06122e48283990fc6db578e1" - integrity sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw== +esbuild-linux-ppc64le@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz#7ec9048502de46754567e734aae7aebd2df6df02" + integrity sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ== -esbuild-netbsd-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz#1ea7fc8cfce88a20a4047b867ef184049a6641ae" - integrity sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA== +esbuild-netbsd-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz#439bdaefffa03a8fa84324f5d83d636f548a2de3" + integrity sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ== -esbuild-openbsd-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz#adde32f2f1b05dc4bd4fc544d6ea5a4379f9ca4d" - integrity sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ== +esbuild-openbsd-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz#c9958e5291a00a3090c1ec482d6bcdf2d5b5d107" + integrity sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w== esbuild-serve@^1.0.1: version "1.0.1" @@ -6555,48 +6547,48 @@ esbuild-serve@^1.0.1: create-serve "^1.0.1" esbuild "^0.9.0" -esbuild-sunos-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz#a7ecaf52b7364fbee76dc8aa707fa3e1cff3342c" - integrity sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw== +esbuild-sunos-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz#ac9ead8287379cd2f6d00bd38c5997fda9c1179e" + integrity sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ== -esbuild-windows-32@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz#a8756033dc905c4b7bea19be69f7ee68809f8770" - integrity sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ== +esbuild-windows-32@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz#a3820fc86631ca594cb7b348514b5cc3f058cfd6" + integrity sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw== -esbuild-windows-64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz#ae694aa66ca078acb8509b2da31197ed1f40f798" - integrity sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ== +esbuild-windows-64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz#1da748441f228d75dff474ddb7d584b81887323c" + integrity sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA== -esbuild-windows-arm64@0.13.12: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz#782c5a8bd6d717ea55aaafe648f9926ca36a4a88" - integrity sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ== +esbuild-windows-arm64@0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz#06dfa52a6b178a5932a9a6e2fdb240c09e6da30c" + integrity sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A== esbuild@^0.13.1, esbuild@^0.13.8: - version "0.13.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.12.tgz#9cac641594bf03cf34145258c093d743ebbde7ca" - integrity sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow== + version "0.13.13" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.13.tgz#0b5399c20f219f663c8c1048436fb0f59ab17a41" + integrity sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q== optionalDependencies: - esbuild-android-arm64 "0.13.12" - esbuild-darwin-64 "0.13.12" - esbuild-darwin-arm64 "0.13.12" - esbuild-freebsd-64 "0.13.12" - esbuild-freebsd-arm64 "0.13.12" - esbuild-linux-32 "0.13.12" - esbuild-linux-64 "0.13.12" - esbuild-linux-arm "0.13.12" - esbuild-linux-arm64 "0.13.12" - esbuild-linux-mips64le "0.13.12" - esbuild-linux-ppc64le "0.13.12" - esbuild-netbsd-64 "0.13.12" - esbuild-openbsd-64 "0.13.12" - esbuild-sunos-64 "0.13.12" - esbuild-windows-32 "0.13.12" - esbuild-windows-64 "0.13.12" - esbuild-windows-arm64 "0.13.12" + esbuild-android-arm64 "0.13.13" + esbuild-darwin-64 "0.13.13" + esbuild-darwin-arm64 "0.13.13" + esbuild-freebsd-64 "0.13.13" + esbuild-freebsd-arm64 "0.13.13" + esbuild-linux-32 "0.13.13" + esbuild-linux-64 "0.13.13" + esbuild-linux-arm "0.13.13" + esbuild-linux-arm64 "0.13.13" + esbuild-linux-mips64le "0.13.13" + esbuild-linux-ppc64le "0.13.13" + esbuild-netbsd-64 "0.13.13" + esbuild-openbsd-64 "0.13.13" + esbuild-sunos-64 "0.13.13" + esbuild-windows-32 "0.13.13" + esbuild-windows-64 "0.13.13" + esbuild-windows-arm64 "0.13.13" esbuild@^0.9.0: version "0.9.7" @@ -6623,7 +6615,7 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -6679,7 +6671,7 @@ eslint-import-resolver-typescript@^2.4.0: resolve "^1.20.0" tsconfig-paths "^3.9.0" -eslint-module-utils@^2.7.0: +eslint-module-utils@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c" integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ== @@ -6689,18 +6681,18 @@ eslint-module-utils@^2.7.0: pkg-dir "^2.0.0" eslint-plugin-import@^2.22.1: - version "2.25.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.2.tgz#b3b9160efddb702fc1636659e71ba1d10adbe9e9" - integrity sha512-qCwQr9TYfoBHOFcVGKY9C9unq05uOxxdklmBXLVvcwo68y5Hta6/GzCZEMx2zQiu0woKNEER0LE7ZgaOfBU14g== + version "2.25.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" + integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg== dependencies: array-includes "^3.1.4" array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.0" + eslint-module-utils "^2.7.1" has "^1.0.3" - is-core-module "^2.7.0" + is-core-module "^2.8.0" is-glob "^4.0.3" minimatch "^3.0.4" object.values "^1.1.5" @@ -6708,46 +6700,47 @@ eslint-plugin-import@^2.22.1: tsconfig-paths "^3.11.0" eslint-plugin-jsx-a11y@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd" - integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg== + version "6.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" + integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== dependencies: - "@babel/runtime" "^7.11.2" + "@babel/runtime" "^7.16.3" aria-query "^4.2.2" - array-includes "^3.1.1" + array-includes "^3.1.4" ast-types-flow "^0.0.7" - axe-core "^4.0.2" + axe-core "^4.3.5" axobject-query "^2.2.0" - damerau-levenshtein "^1.0.6" - emoji-regex "^9.0.0" + damerau-levenshtein "^1.0.7" + emoji-regex "^9.2.2" has "^1.0.3" - jsx-ast-utils "^3.1.0" + jsx-ast-utils "^3.2.1" language-tags "^1.0.5" + minimatch "^3.0.4" eslint-plugin-react-hooks@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" - integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" + integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== eslint-plugin-react@^7.23.1: - version "7.26.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.26.1.tgz#41bcfe3e39e6a5ac040971c1af94437c80daa40e" - integrity sha512-Lug0+NOFXeOE+ORZ5pbsh6mSKjBKXDXItUD2sQoT+5Yl0eoT82DqnXeTMfUare4QVCn9QwXbfzO/dBLjLXwVjQ== + version "7.27.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.27.0.tgz#f952c76517a3915b81c7788b220b2b4c96703124" + integrity sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg== dependencies: - array-includes "^3.1.3" - array.prototype.flatmap "^1.2.4" + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" doctrine "^2.1.0" - estraverse "^5.2.0" + estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.0.4" - object.entries "^1.1.4" - object.fromentries "^2.0.4" - object.hasown "^1.0.0" - object.values "^1.1.4" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" prop-types "^15.7.2" resolve "^2.0.0-next.3" semver "^6.3.0" - string.prototype.matchall "^4.0.5" + string.prototype.matchall "^4.0.6" eslint-scope@^5.1.1: version "5.1.1" @@ -6860,7 +6853,7 @@ estraverse@^4.1.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0, estraverse@^5.2.0: +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== @@ -7111,11 +7104,6 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figlet@^1.1.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634" - integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ== - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -7276,9 +7264,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== + version "3.2.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== flatten@^1.0.2: version "1.0.3" @@ -7833,13 +7821,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -8442,7 +8423,7 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.7.0: +is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== @@ -9493,10 +9474,10 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonpointer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" - integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg== +jsonpointer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" + integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== jsonwebtoken@^8.5.1: version "8.5.1" @@ -9524,7 +9505,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== @@ -10318,17 +10299,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.50.0, "mime-db@>= 1.43.0 < 2": - version "1.50.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" - integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== +mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.33" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" - integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: - mime-db "1.50.0" + mime-db "1.51.0" mime@^1.3.4: version "1.6.0" @@ -11027,7 +11008,7 @@ oauth@^0.9.15: resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -11076,7 +11057,7 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.4: +object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== @@ -11085,7 +11066,7 @@ object.entries@^1.1.4: define-properties "^1.1.3" es-abstract "^1.19.1" -object.fromentries@^2.0.4: +object.fromentries@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== @@ -11103,7 +11084,7 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.3" es-abstract "^1.19.1" -object.hasown@^1.0.0: +object.hasown@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== @@ -11118,7 +11099,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.4, object.values@^1.1.5: +object.values@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== @@ -11247,7 +11228,7 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-limit@3.1.0, p-limit@^3.0.2, p-limit@^3.1.0: +p-limit@3.1.0, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -11380,11 +11361,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parent-require@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parent-require/-/parent-require-1.0.0.tgz#746a167638083a860b0eef6732cb27ed46c32977" - integrity sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc= - parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" @@ -12707,9 +12683,9 @@ rollup-plugin-terser@^7.0.0: terser "^5.0.0" rollup@^2.43.1: - version "2.59.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.59.0.tgz#108c61b0fa0a37ebc8d1f164f281622056f0db59" - integrity sha512-l7s90JQhCQ6JyZjKgo7Lq1dKh2RxatOM+Jr6a9F7WbS9WgKbocyUSeLmZl8evAse7y96Ae98L2k1cBOwWD8nHw== + version "2.60.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.0.tgz#4ee60ab7bdd0356763f87d7099f413e5460fc193" + integrity sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ== optionalDependencies: fsevents "~2.3.2" @@ -13416,7 +13392,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string.prototype.matchall@^4.0.5: +string.prototype.matchall@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== @@ -13620,11 +13596,6 @@ supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0: dependencies: has-flag "^4.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -13764,12 +13735,11 @@ terminal-link@^2.0.0: supports-hyperlinks "^2.0.0" terser-webpack-plugin@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz#ad1be7639b1cbe3ea49fab995cbe7224b31747a1" - integrity sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA== + version "5.2.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz#ce65b9880a0c36872555c4874f45bbdb02ee32c9" + integrity sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g== dependencies: jest-worker "^27.0.6" - p-limit "^3.1.0" schema-utils "^3.1.1" serialize-javascript "^6.0.0" source-map "^0.6.1" @@ -14190,9 +14160,9 @@ typedoc@^0.22.3: shiki "^0.9.12" typeorm@^0.2.30: - version "0.2.38" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.38.tgz#2af08079919f6ab04cd17017f9faa2c8d5cd566f" - integrity sha512-M6Y3KQcAREQcphOVJciywf4mv6+A0I/SeR+lWNjKsjnQ+a3XcMwGYMGL0Jonsx3H0Cqlf/3yYqVki1jIXSK/xg== + version "0.2.40" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.40.tgz#5a5168b9ad4f01b539c9a09a9fbdb65631928b32" + integrity sha512-F1WQKFl3opokSBNxUdX68uhD1Hpk8mgigsLtJKluI7YRs52+tsHNuaO5gLSzvjYZoLqnYc6pomsCYZMRUT2zxA== dependencies: "@sqltools/formatter" "^1.2.2" app-root-path "^3.0.0" @@ -14208,7 +14178,6 @@ typeorm@^0.2.30: sha.js "^2.4.11" tslib "^2.1.0" xml2js "^0.4.23" - yargonaut "^1.1.4" yargs "^17.0.1" zen-observable-ts "^1.0.0" @@ -14525,7 +14494,7 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -vary@^1.1.2, vary@~1.1.2: +vary@^1, vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -14554,9 +14523,9 @@ vm-browserify@1.1.2: integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== vsce@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/vsce/-/vsce-2.2.0.tgz#8e78b005d27909d2f903744d374a750ce7bf4aa3" - integrity sha512-Ww4P5MibbMorrRU5Dugd/IhGpq8inCtgxrFko7Yo8tIPlEq9LrjVD9/P7+s6gKMnlpGpvfC6xmR51cPb4Uf+nQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/vsce/-/vsce-2.3.0.tgz#3ab295cc5f7d19a426ed32888d8ecabd25e5606d" + integrity sha512-nmbczr1rC+lRikX1NYMoTFX6CqPlfk11f7LbRgdjpa6zkLNndlTtnpvOawj7NYkw5jmy+5bGHMGt4DIimZXZmg== dependencies: azure-devops-node-api "^11.0.1" chalk "^2.4.2" @@ -15096,15 +15065,6 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargonaut@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/yargonaut/-/yargonaut-1.1.4.tgz#c64f56432c7465271221f53f5cc517890c3d6e0c" - integrity sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA== - dependencies: - chalk "^1.1.1" - figlet "^1.1.1" - parent-require "^1.0.0" - yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" @@ -15215,6 +15175,6 @@ zen-observable@0.8.15: integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== zustand@^3.6.4: - version "3.6.4" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.6.4.tgz#2aed404072e3d4538fbc380bcf4eee88b174051d" - integrity sha512-liH2ZaEOSiEaVEl7N0CVzoKYZCQPpibfsIgB2ksrjvfu17WME8Eh7XV/MCi5OQM5AnbuYbLowplR03UP5yrNYw== + version "3.6.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.6.5.tgz#42a459397907d6bf0e2375351394733b2f83ee44" + integrity sha512-/WfLJuXiEJimt61KGMHebrFBwckkCHGhAgVXTgPQHl6IMzjqm6MREb1OnDSnCRiSmRdhgdFCctceg6tSm79hiw==