[refactor] events (#230)
* bumps rko, adds events * rename tlstate to state, fix env for multiplayer test * Fix multiplayer * rename data tldrawstate to tldrawsnapshot * Update multiplayer-editor.tsx * Fix shhhmp * Update 2.tldr * Add API to the README
This commit is contained in:
parent
f037118928
commit
be2c6d6d1f
131 changed files with 3938 additions and 3674 deletions
143
README.md
143
README.md
|
@ -32,7 +32,21 @@ function App() {
|
|||
}
|
||||
```
|
||||
|
||||
You can control the `TLDraw` component through props:
|
||||
### Persisting the State
|
||||
|
||||
You can use the `id` to persist the state in a user's browser storage.
|
||||
|
||||
```tsx
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
return <TLDraw id="myState" />
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling the Component through Props
|
||||
|
||||
You can control the `TLDraw` component through its props.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
|
||||
|
@ -44,42 +58,65 @@ function App() {
|
|||
}
|
||||
```
|
||||
|
||||
Or imperatively through the `TLDrawState` instance:
|
||||
### Controlling the Component through the TLDrawState API
|
||||
|
||||
You can also control the `TLDraw` component imperatively through the `TLDrawState` API.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleMount = React.useCallback((tlstate: TLDrawState) => {
|
||||
const myDocument: TLDrawDocument = {}
|
||||
|
||||
tlstate.loadDocument(myDocument).selectAll()
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
state.selectAll()
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section for more on this API.
|
||||
|
||||
### Responding to Changes
|
||||
|
||||
You can respond to changes and user actions using the `onChange` callback.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleChange = React.useCallback((state: TLDrawState, reason: string) => {}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section for more on this API.
|
||||
|
||||
## Documentation
|
||||
|
||||
### `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.**
|
||||
|
||||
| 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. |
|
||||
| `onMount` | `Function` | A callback function that will be called when the editor first mounts, receiving the current `TLDrawState`. |
|
||||
| `onChange` | `Function` | A callback function that will be called whenever the `TLDrawState` updates. The update will include the current `TLDrawState` and the reason for the change. |
|
||||
| `onUserChange` | `Function` | A callback function that will be fired when the user's "presence" information changes. |
|
||||
| `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. |
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. |
|
||||
| `onRedo` | `Function` | Called when the `TLDrawState` updates after a redo. |
|
||||
| `onUserChange` | `Function` | Called when the user's "presence" information changes. |
|
||||
| `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. |
|
||||
|
||||
### `TLDrawDocument`
|
||||
|
||||
|
@ -215,6 +252,72 @@ 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
|
||||
|
||||
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`.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
state.selectAll()
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
The `TLDrawState` API is too large to document here. To view documentation for the API, build the documentation by running `yarn docs` from the root folder and open the file at `/packages/tldraw/docs/classes/TLDrawState.html` in your browser.
|
||||
|
||||
Here are some useful methods:
|
||||
|
||||
| Method | Description |
|
||||
| ----------------- | ----------- |
|
||||
| `loadDocument` | |
|
||||
| `select` | |
|
||||
| `selectAll` | |
|
||||
| `selectNone` | |
|
||||
| `delete` | |
|
||||
| `deleteAll` | |
|
||||
| `deletePage` | |
|
||||
| `changePage` | |
|
||||
| `cut` | |
|
||||
| `copy` | |
|
||||
| `paste` | |
|
||||
| `copyJson` | |
|
||||
| `copySvg` | |
|
||||
| `undo` | |
|
||||
| `redo` | |
|
||||
| `zoomIn` | |
|
||||
| `zoomOut` | |
|
||||
| `zoomToContent` | |
|
||||
| `zoomToSelection` | |
|
||||
| `zoomToFit` | |
|
||||
| `zoomTo` | |
|
||||
| `resetZoom` | |
|
||||
| `setCamera` | |
|
||||
| `resetCamera` | |
|
||||
| `align` | |
|
||||
| `distribute` | |
|
||||
| `stretch` | |
|
||||
| `nudge` | |
|
||||
| `duplicate` | |
|
||||
| `flipHorizontal` | |
|
||||
| `flipVertical` | |
|
||||
| `rotate` | |
|
||||
| `style` | |
|
||||
| `group` | |
|
||||
| `ungroup` | |
|
||||
| `createShapes` | |
|
||||
| `updateShapes` | |
|
||||
| `updateDocument` | |
|
||||
| `updateUsers` | |
|
||||
| `removeUser` | |
|
||||
| `setSetting` | |
|
||||
| `selectTool` | |
|
||||
| `cancel` | |
|
||||
|
||||
## Local Development
|
||||
|
||||
- Run `yarn` to install dependencies.
|
||||
|
|
|
@ -6,67 +6,67 @@ import type { Message, TLApi } from 'src/types'
|
|||
export default function App(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
|
||||
// When the editor mounts, save the tlstate instance in a ref.
|
||||
// When the editor mounts, save the state instance in a ref.
|
||||
const handleMount = React.useCallback((tldr: TLDrawState) => {
|
||||
rTLDrawState.current = tldr
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleEvent(message: Message) {
|
||||
const tlstate = rTLDrawState.current
|
||||
if (!tlstate) return
|
||||
const state = rTLDrawState.current
|
||||
if (!state) return
|
||||
|
||||
switch (message.type) {
|
||||
case 'resetZoom': {
|
||||
tlstate.resetZoom()
|
||||
state.resetZoom()
|
||||
break
|
||||
}
|
||||
case 'zoomIn': {
|
||||
tlstate.zoomIn()
|
||||
state.zoomIn()
|
||||
break
|
||||
}
|
||||
case 'zoomOut': {
|
||||
tlstate.zoomOut()
|
||||
state.zoomOut()
|
||||
break
|
||||
}
|
||||
case 'zoomToFit': {
|
||||
tlstate.zoomToFit()
|
||||
state.zoomToFit()
|
||||
break
|
||||
}
|
||||
case 'zoomToSelection': {
|
||||
tlstate.zoomToSelection()
|
||||
state.zoomToSelection()
|
||||
break
|
||||
}
|
||||
case 'undo': {
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
break
|
||||
}
|
||||
case 'redo': {
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
break
|
||||
}
|
||||
case 'cut': {
|
||||
tlstate.cut()
|
||||
state.cut()
|
||||
break
|
||||
}
|
||||
case 'copy': {
|
||||
tlstate.copy()
|
||||
state.copy()
|
||||
break
|
||||
}
|
||||
case 'paste': {
|
||||
tlstate.paste()
|
||||
state.paste()
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
tlstate.delete()
|
||||
state.delete()
|
||||
break
|
||||
}
|
||||
case 'selectAll': {
|
||||
tlstate.selectAll()
|
||||
state.selectAll()
|
||||
break
|
||||
}
|
||||
case 'selectNone': {
|
||||
tlstate.selectNone()
|
||||
state.selectNone()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/* eslint-disable no-undef */
|
||||
import fs from 'fs'
|
||||
import esbuild from 'esbuild'
|
||||
import serve, { error, log } from 'create-serve'
|
||||
|
||||
if (!fs.existsSync('./dist')) {
|
||||
fs.mkdirSync('./dist')
|
||||
}
|
||||
|
||||
fs.copyFile('./src/index.html', './dist/index.html', (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ['src/index.tsx'],
|
||||
outfile: 'dist/index.js',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
incremental: true,
|
||||
format: 'esm',
|
||||
target: 'esnext',
|
||||
define: {
|
||||
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': process.env.LIVEBLOCKS_PUBLIC_API_KEY,
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
watch: {
|
||||
onRebuild(err) {
|
||||
serve.update()
|
||||
err ? error('❌ Failed') : log('✅ Updated')
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch(() => process.exit(1))
|
||||
|
||||
serve.start({
|
||||
port: 5420,
|
||||
root: './dist',
|
||||
live: true,
|
||||
})
|
|
@ -24,7 +24,9 @@
|
|||
"@types/react-router-dom": "^5.1.8",
|
||||
"concurrently": "6.0.1",
|
||||
"create-serve": "1.0.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild-envfile-plugin": "^1.0.1",
|
||||
"esbuild-serve": "^1.0.1",
|
||||
"react": ">=16.8",
|
||||
"react-dom": "^16.8 || ^17.0",
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
/* eslint-disable no-undef */
|
||||
import fs from 'fs'
|
||||
import esbuildServe from 'esbuild-serve'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync('./dist')) {
|
||||
|
@ -11,6 +14,8 @@ async function main() {
|
|||
if (err) throw err
|
||||
})
|
||||
|
||||
console.log(process.env.LIVEBLOCKS_PUBLIC_API_KEY)
|
||||
|
||||
try {
|
||||
await esbuildServe(
|
||||
{
|
||||
|
@ -20,9 +25,11 @@ async function main() {
|
|||
bundle: true,
|
||||
sourcemap: true,
|
||||
incremental: true,
|
||||
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
||||
format: 'cjs',
|
||||
target: 'es6',
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': `"${process.env.LIVEBLOCKS_PUBLIC_API_KEY}"`,
|
||||
},
|
||||
watch: {
|
||||
onRebuild(err) {
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function Editor(props: TLDrawProps): JSX.Element {
|
|||
props.onMount?.(state)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.tlstate = state
|
||||
window.state = state
|
||||
}, [])
|
||||
|
||||
const onSignIn = React.useCallback((state: TLDrawState) => {
|
||||
|
|
|
@ -101,8 +101,8 @@ export default function Controlled() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const handleChange = React.useCallback((tlstate) => {
|
||||
rDocument.current = tlstate.document
|
||||
const handleChange = React.useCallback((state) => {
|
||||
rDocument.current = state.document
|
||||
}, [])
|
||||
|
||||
return <TLDraw document={doc} onChange={handleChange} />
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ColorStyle, TLDraw, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw
|
|||
|
||||
export default function Imperative(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
rTLDrawState.current = state
|
||||
|
||||
|
@ -29,17 +30,24 @@ export default function Imperative(): JSX.Element {
|
|||
React.useEffect(() => {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
const tlstate = rTLDrawState.current!
|
||||
const rect1 = tlstate.getShape('rect1')
|
||||
const state = rTLDrawState.current!
|
||||
const rect1 = state.getShape('rect1')
|
||||
|
||||
if (!rect1) {
|
||||
// clearInterval(interval)
|
||||
state.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const color = i % 2 ? ColorStyle.Red : ColorStyle.Blue
|
||||
|
||||
tlstate.patchShapes({
|
||||
state.patchShapes({
|
||||
id: 'rect1',
|
||||
style: {
|
||||
...rect1.style,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser, Data } from '@tldraw/tldraw'
|
||||
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
|
||||
import { createClient, Presence } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
@ -9,19 +9,17 @@ interface TLDrawUserPresence extends Presence {
|
|||
user: TLDrawUser
|
||||
}
|
||||
|
||||
const publicAPIKey = 'pk_live_1LJGGaqBSNLjLT-4Jalkl-U9'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: publicAPIKey,
|
||||
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 80,
|
||||
})
|
||||
|
||||
const ROOM_ID = 'mp-test-2'
|
||||
const roomId = 'mp-test-2'
|
||||
|
||||
export function Multiplayer() {
|
||||
return (
|
||||
<LiveblocksProvider client={client}>
|
||||
<RoomProvider id={ROOM_ID}>
|
||||
<RoomProvider id={roomId}>
|
||||
<TLDrawWrapper />
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
|
@ -33,7 +31,7 @@ function TLDrawWrapper() {
|
|||
|
||||
const [error, setError] = React.useState<Error>()
|
||||
|
||||
const [tlstate, setTlstate] = React.useState<TLDrawState>()
|
||||
const [state, setstate] = React.useState<TLDrawState>()
|
||||
|
||||
useErrorListener((err) => setError(err))
|
||||
|
||||
|
@ -41,55 +39,38 @@ function TLDrawWrapper() {
|
|||
uuid: docId,
|
||||
document: {
|
||||
...TLDrawState.defaultDocument,
|
||||
id: 'test-room',
|
||||
id: roomId,
|
||||
},
|
||||
})
|
||||
|
||||
// Put the tlstate into the window, for debugging.
|
||||
const handleMount = React.useCallback((tlstate: TLDrawState) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.tlstate = tlstate
|
||||
|
||||
tlstate.loadRoom(ROOM_ID)
|
||||
|
||||
setTlstate(tlstate)
|
||||
}, [])
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(_tlstate: TLDrawState, state: Data, reason: string) => {
|
||||
// If the client updates its document, update the room's document
|
||||
if (reason.startsWith('command') || reason.startsWith('undo') || reason.startsWith('redo')) {
|
||||
doc?.update({ uuid: docId, document: state.document })
|
||||
}
|
||||
|
||||
// When the client updates its presence, update the room
|
||||
// if (state.room && (reason === 'patch:room:self:update' || reason === 'patch:selected')) {
|
||||
// const room = client.getRoom(ROOM_ID)
|
||||
// if (!room) return
|
||||
// const { userId, users } = state.room
|
||||
// room.updatePresence({ id: userId, user: users[userId] })
|
||||
// }
|
||||
// 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)
|
||||
},
|
||||
[docId, doc]
|
||||
[roomId]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const room = client.getRoom(ROOM_ID)
|
||||
const room = client.getRoom(roomId)
|
||||
|
||||
if (!room) return
|
||||
if (!doc) return
|
||||
if (!tlstate) return
|
||||
if (!tlstate.state.room) return
|
||||
if (!state) return
|
||||
if (!state.state.room) return
|
||||
|
||||
// Update the user's presence with the user from state
|
||||
const { users, userId } = tlstate.state.room
|
||||
const { users, userId } = state.state.room
|
||||
|
||||
room.updatePresence({ id: userId, user: users[userId] })
|
||||
|
||||
// Subscribe to presence changes; when others change, update the state
|
||||
room.subscribe<TLDrawUserPresence>('others', (others) => {
|
||||
tlstate.updateUsers(
|
||||
state.updateUsers(
|
||||
others
|
||||
.toArray()
|
||||
.filter((other) => other.presence)
|
||||
|
@ -100,30 +81,26 @@ function TLDrawWrapper() {
|
|||
|
||||
room.subscribe('event', (event) => {
|
||||
if (event.event?.name === 'exit') {
|
||||
tlstate.removeUser(event.event.userId)
|
||||
state.removeUser(event.event.userId)
|
||||
}
|
||||
})
|
||||
|
||||
function handleDocumentUpdates() {
|
||||
if (!doc) return
|
||||
if (!tlstate) return
|
||||
if (!tlstate.state.room) return
|
||||
if (!state) return
|
||||
if (!state.state.room) return
|
||||
|
||||
const docObject = doc.toObject()
|
||||
|
||||
// Only merge the change if it caused by someone else
|
||||
if (docObject.uuid !== docId) {
|
||||
tlstate.mergeDocument(docObject.document)
|
||||
state.mergeDocument(docObject.document)
|
||||
} else {
|
||||
tlstate.updateUsers(
|
||||
Object.values(tlstate.state.room.users).map((user) => {
|
||||
// const activeShapes = user.activeShapes
|
||||
// .map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
|
||||
// .filter(Boolean)
|
||||
state.updateUsers(
|
||||
Object.values(state.state.room.users).map((user) => {
|
||||
return {
|
||||
...user,
|
||||
// activeShapes: activeShapes,
|
||||
selectedIds: user.selectedIds, // activeShapes.map((shape) => shape.id),
|
||||
selectedIds: user.selectedIds,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -131,8 +108,8 @@ function TLDrawWrapper() {
|
|||
}
|
||||
|
||||
function handleExit() {
|
||||
if (!(tlstate && tlstate.state.room)) return
|
||||
room?.broadcastEvent({ name: 'exit', userId: tlstate.state.room.userId })
|
||||
if (!(state && state.state.room)) return
|
||||
room?.broadcastEvent({ name: 'exit', userId: state.state.room.userId })
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleExit)
|
||||
|
@ -141,18 +118,29 @@ function TLDrawWrapper() {
|
|||
doc.subscribe(handleDocumentUpdates)
|
||||
|
||||
// Load the shared document
|
||||
tlstate.loadDocument(doc.toObject().document)
|
||||
const newDocument = doc.toObject().document
|
||||
|
||||
if (newDocument) {
|
||||
state.loadDocument(newDocument)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleExit)
|
||||
doc.unsubscribe(handleDocumentUpdates)
|
||||
}
|
||||
}, [doc, docId, tlstate])
|
||||
}, [doc, docId, state])
|
||||
|
||||
const handlePersist = React.useCallback(
|
||||
(state: TLDrawState) => {
|
||||
doc?.update({ uuid: docId, document: state.document })
|
||||
},
|
||||
[docId, doc]
|
||||
)
|
||||
|
||||
const handleUserChange = React.useCallback(
|
||||
(tlstate: TLDrawState, user: TLDrawUser) => {
|
||||
const room = client.getRoom(ROOM_ID)
|
||||
room?.updatePresence({ id: tlstate.state.room?.userId, user })
|
||||
(state: TLDrawState, user: TLDrawUser) => {
|
||||
const room = client.getRoom(roomId)
|
||||
room?.updatePresence({ id: state.state.room?.userId, user })
|
||||
},
|
||||
[client]
|
||||
)
|
||||
|
@ -165,7 +153,7 @@ function TLDrawWrapper() {
|
|||
<div className="tldraw">
|
||||
<TLDraw
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
onPersist={handlePersist}
|
||||
onUserChange={handleUserChange}
|
||||
showPages={false}
|
||||
/>
|
||||
|
|
|
@ -396,7 +396,7 @@
|
|||
"signature": "1444afb2d3c50b5a15354934187d75bc9a7ca2d10bf20fe9c79cbcd1f8548549",
|
||||
"affectsGlobalScope": false
|
||||
},
|
||||
"../tldraw/dist/types/state/tlstate.d.ts": {
|
||||
"../tldraw/dist/types/state/state.d.ts": {
|
||||
"version": "3a31dc5b6306ee6cff5e03e4a3ab1eda22f07231bc207f77807915e7c01a96a9",
|
||||
"signature": "3a31dc5b6306ee6cff5e03e4a3ab1eda22f07231bc207f77807915e7c01a96a9",
|
||||
"affectsGlobalScope": false
|
||||
|
@ -1245,9 +1245,9 @@
|
|||
"../tldraw/dist/types/types.d.ts"
|
||||
],
|
||||
"../tldraw/dist/types/state/index.d.ts": [
|
||||
"../tldraw/dist/types/state/tlstate.d.ts"
|
||||
"../tldraw/dist/types/state/state.d.ts"
|
||||
],
|
||||
"../tldraw/dist/types/state/tlstate.d.ts": [
|
||||
"../tldraw/dist/types/state/state.d.ts": [
|
||||
"../../node_modules/rko/dist/types/index.d.ts",
|
||||
"../core/dist/types/index.d.ts",
|
||||
"../tldraw/dist/types/types.d.ts"
|
||||
|
@ -1621,9 +1621,9 @@
|
|||
"../tldraw/dist/types/types.d.ts"
|
||||
],
|
||||
"../tldraw/dist/types/state/index.d.ts": [
|
||||
"../tldraw/dist/types/state/tlstate.d.ts"
|
||||
"../tldraw/dist/types/state/state.d.ts"
|
||||
],
|
||||
"../tldraw/dist/types/state/tlstate.d.ts": [
|
||||
"../tldraw/dist/types/state/state.d.ts": [
|
||||
"../../node_modules/rko/dist/types/index.d.ts",
|
||||
"../core/dist/types/index.d.ts",
|
||||
"../tldraw/dist/types/types.d.ts"
|
||||
|
@ -1792,7 +1792,7 @@
|
|||
"../tldraw/dist/types/shape/shape-styles.d.ts",
|
||||
"../tldraw/dist/types/shape/shape-utils.d.ts",
|
||||
"../tldraw/dist/types/state/index.d.ts",
|
||||
"../tldraw/dist/types/state/tlstate.d.ts",
|
||||
"../tldraw/dist/types/state/state.d.ts",
|
||||
"../tldraw/dist/types/types.d.ts"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
## 0.1.6
|
||||
## 0.1.4
|
||||
|
||||
- UI bug fixes.
|
||||
|
||||
|
|
|
@ -32,7 +32,21 @@ function App() {
|
|||
}
|
||||
```
|
||||
|
||||
You can control the `TLDraw` component through props:
|
||||
### Persisting the State
|
||||
|
||||
You can use the `id` to persist the state in a user's browser storage.
|
||||
|
||||
```tsx
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
return <TLDraw id="myState" />
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling the Component through Props
|
||||
|
||||
You can control the `TLDraw` component through its props.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
|
||||
|
@ -44,22 +58,40 @@ function App() {
|
|||
}
|
||||
```
|
||||
|
||||
Or imperatively through the `TLDrawState` instance:
|
||||
### Controlling the Component through the TLDrawState API
|
||||
|
||||
You can also control the `TLDraw` component imperatively through the `TLDrawState` API.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleMount = React.useCallback((tlstate: TLDrawState) => {
|
||||
const myDocument: TLDrawDocument = {}
|
||||
|
||||
tlstate.loadDocument(myDocument).selectAll()
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
state.selectAll()
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section for more on this API.
|
||||
|
||||
### Responding to Changes
|
||||
|
||||
You can respond to changes and user actions using the `onChange` callback.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleChange = React.useCallback((state: TLDrawState, reason: string) => {}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section for more on this API.
|
||||
|
||||
## Documentation
|
||||
|
||||
### `TLDraw`
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"@tldraw/vec": "^0.1.3",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
"rko": "^0.6.0",
|
||||
"rko": "^0.6.2",
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as React from 'react'
|
|||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import { styled, dark } from '~styles'
|
||||
import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
||||
import { TLDrawState } from '~state'
|
||||
import { TLDrawSnapshot, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
||||
import { TLDrawCallbacks, TLDrawState } from '~state'
|
||||
import {
|
||||
TLDrawContext,
|
||||
TLDrawContextType,
|
||||
|
@ -19,9 +19,9 @@ import { ContextMenu } from '~components/ContextMenu'
|
|||
import { FocusButton } from '~components/FocusButton/FocusButton'
|
||||
|
||||
// Selectors
|
||||
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
|
||||
const isInSelectSelector = (s: TLDrawSnapshot) => s.appState.activeTool === 'select'
|
||||
|
||||
const isHideBoundsShapeSelector = (s: Data) => {
|
||||
const isHideBoundsShapeSelector = (s: TLDrawSnapshot) => {
|
||||
const { shapes } = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return (
|
||||
|
@ -30,17 +30,17 @@ const isHideBoundsShapeSelector = (s: Data) => {
|
|||
)
|
||||
}
|
||||
|
||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
||||
const pageSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId]
|
||||
|
||||
const snapLinesSelector = (s: Data) => s.appState.snapLines
|
||||
const snapLinesSelector = (s: TLDrawSnapshot) => s.appState.snapLines
|
||||
|
||||
const usersSelector = (s: Data) => s.room?.users
|
||||
const usersSelector = (s: TLDrawSnapshot) => s.room?.users
|
||||
|
||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||
const pageStateSelector = (s: TLDrawSnapshot) => s.document.pageStates[s.appState.currentPageId]
|
||||
|
||||
const settingsSelector = (s: Data) => s.settings
|
||||
const settingsSelector = (s: TLDrawSnapshot) => s.settings
|
||||
|
||||
export interface TLDrawProps {
|
||||
export interface TLDrawProps extends TLDrawCallbacks {
|
||||
/**
|
||||
* (optional) If provided, the component will load / persist state under this key.
|
||||
*/
|
||||
|
@ -100,12 +100,6 @@ export interface TLDrawProps {
|
|||
* (optional) A callback to run when the component mounts.
|
||||
*/
|
||||
onMount?: (state: TLDrawState) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: TLDrawState['_onChange']
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
|
@ -130,10 +124,35 @@ export interface TLDrawProps {
|
|||
* (optional) A callback to run when the user signs out via the menu.
|
||||
*/
|
||||
onSignOut?: (state: TLDrawState) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project.
|
||||
*/
|
||||
onUserChange?: (state: TLDrawState, user: TLDrawUser) => void
|
||||
/**
|
||||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: (state: TLDrawState, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is patched.
|
||||
*/
|
||||
onPatch?: (state: TLDrawState, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is changed with a command.
|
||||
*/
|
||||
onCommand?: (state: TLDrawState, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is persisted.
|
||||
*/
|
||||
onPersist?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user undos.
|
||||
*/
|
||||
onUndo?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user redos.
|
||||
*/
|
||||
onRedo?: (state: TLDrawState) => void
|
||||
}
|
||||
|
||||
export function TLDraw({
|
||||
|
@ -157,57 +176,85 @@ export function TLDraw({
|
|||
onOpenProject,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPersist,
|
||||
onPatch,
|
||||
onCommand,
|
||||
}: TLDrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
||||
const [tlstate, setTlstate] = React.useState(
|
||||
() => new TLDrawState(id, onMount, onChange, onUserChange)
|
||||
)
|
||||
const [state, setState] = React.useState(() => new TLDrawState(id))
|
||||
|
||||
const [context, setContext] = React.useState<TLDrawContextType>(() => ({
|
||||
tlstate,
|
||||
useSelector: tlstate.useStore,
|
||||
callbacks: {
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onSignIn,
|
||||
onSignOut,
|
||||
},
|
||||
state,
|
||||
useSelector: state.useStore,
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
if (id === sId) return
|
||||
|
||||
// If a new id is loaded, replace the entire state
|
||||
const newState = new TLDrawState(id)
|
||||
|
||||
setSId(id)
|
||||
const newState = new TLDrawState(id, onMount, onChange, onUserChange)
|
||||
setTlstate(newState)
|
||||
|
||||
setContext((ctx) => ({
|
||||
...ctx,
|
||||
tlstate: newState,
|
||||
state: newState,
|
||||
useSelector: newState.useStore,
|
||||
}))
|
||||
|
||||
setState(newState)
|
||||
}, [sId, id])
|
||||
|
||||
// Update the callbacks when any callback changes
|
||||
React.useEffect(() => {
|
||||
setContext((ctx) => ({
|
||||
...ctx,
|
||||
callbacks: {
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onSignIn,
|
||||
onSignOut,
|
||||
},
|
||||
}))
|
||||
}, [onNewProject, onSaveProject, onSaveProjectAs, onOpenProject, onSignIn, onSignOut])
|
||||
state.readOnly = readOnly
|
||||
}, [state, readOnly])
|
||||
|
||||
React.useEffect(() => {
|
||||
tlstate.readOnly = readOnly
|
||||
}, [tlstate, readOnly])
|
||||
if (!document) return
|
||||
|
||||
if (document.id === state.document.id) {
|
||||
state.updateDocument(document)
|
||||
} else {
|
||||
state.loadDocument(document)
|
||||
}
|
||||
}, [document, state])
|
||||
|
||||
React.useEffect(() => {
|
||||
state.callbacks = {
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPatch,
|
||||
onCommand,
|
||||
onPersist,
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPatch,
|
||||
onCommand,
|
||||
onPersist,
|
||||
])
|
||||
|
||||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||
return (
|
||||
|
@ -217,7 +264,6 @@ export function TLDraw({
|
|||
key={sId || 'tldraw'}
|
||||
id={sId}
|
||||
currentPageId={currentPageId}
|
||||
document={document}
|
||||
autofocus={autofocus}
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
|
@ -243,10 +289,9 @@ interface InnerTLDrawProps {
|
|||
showUI: boolean
|
||||
showTools: boolean
|
||||
readOnly: boolean
|
||||
document?: TLDrawDocument
|
||||
}
|
||||
|
||||
function InnerTldraw({
|
||||
const InnerTldraw = React.memo(function InnerTldraw({
|
||||
id,
|
||||
currentPageId,
|
||||
autofocus,
|
||||
|
@ -257,9 +302,8 @@ function InnerTldraw({
|
|||
showTools,
|
||||
readOnly,
|
||||
showUI,
|
||||
document,
|
||||
}: InnerTLDrawProps) {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
|
@ -277,11 +321,11 @@ function InnerTldraw({
|
|||
|
||||
const isHideBoundsShape = useSelector(isHideBoundsShapeSelector)
|
||||
|
||||
const isInSession = tlstate.session !== undefined
|
||||
const isInSession = state.session !== undefined
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && tlstate.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && state.session?.constructor.name !== 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId
|
||||
|
@ -291,7 +335,7 @@ function InnerTldraw({
|
|||
|
||||
// Hide indicators when not using the select tool, or when in session
|
||||
const hideIndicators =
|
||||
(isInSession && tlstate.appState.status !== TLDrawStatus.Brushing) || !isSelecting
|
||||
(isInSession && state.appState.status !== TLDrawStatus.Brushing) || !isSelecting
|
||||
|
||||
// Custom rendering meta, with dark mode for shapes
|
||||
const meta = React.useMemo(() => ({ isDarkMode: settings.isDarkMode }), [settings.isDarkMode])
|
||||
|
@ -312,22 +356,10 @@ function InnerTldraw({
|
|||
return {}
|
||||
}, [settings.isDarkMode])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!document) return
|
||||
|
||||
if (document.id === tlstate.document.id) {
|
||||
console.log('updating')
|
||||
tlstate.updateDocument(document)
|
||||
} else {
|
||||
console.log('loading')
|
||||
tlstate.loadDocument(document)
|
||||
}
|
||||
}, [document, tlstate])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentPageId) return
|
||||
tlstate.changePage(currentPageId)
|
||||
}, [currentPageId, tlstate])
|
||||
state.changePage(currentPageId)
|
||||
}, [currentPageId, state])
|
||||
|
||||
return (
|
||||
<StyledLayout ref={rWrapper} tabIndex={0} className={settings.isDarkMode ? dark : ''}>
|
||||
|
@ -341,7 +373,7 @@ function InnerTldraw({
|
|||
pageState={pageState}
|
||||
snapLines={snapLines}
|
||||
users={users}
|
||||
userId={tlstate.state.room?.userId}
|
||||
userId={state.state.room?.userId}
|
||||
theme={theme}
|
||||
meta={meta}
|
||||
hideBounds={hideBounds}
|
||||
|
@ -350,61 +382,61 @@ function InnerTldraw({
|
|||
hideBindingHandles={!settings.showBindingHandles}
|
||||
hideCloneHandles={!settings.showCloneHandles}
|
||||
hideRotateHandles={!settings.showRotateHandles}
|
||||
onPinchStart={tlstate.onPinchStart}
|
||||
onPinchEnd={tlstate.onPinchEnd}
|
||||
onPinch={tlstate.onPinch}
|
||||
onPan={tlstate.onPan}
|
||||
onZoom={tlstate.onZoom}
|
||||
onPointerDown={tlstate.onPointerDown}
|
||||
onPointerMove={tlstate.onPointerMove}
|
||||
onPointerUp={tlstate.onPointerUp}
|
||||
onPointCanvas={tlstate.onPointCanvas}
|
||||
onDoubleClickCanvas={tlstate.onDoubleClickCanvas}
|
||||
onRightPointCanvas={tlstate.onRightPointCanvas}
|
||||
onDragCanvas={tlstate.onDragCanvas}
|
||||
onReleaseCanvas={tlstate.onReleaseCanvas}
|
||||
onPointShape={tlstate.onPointShape}
|
||||
onDoubleClickShape={tlstate.onDoubleClickShape}
|
||||
onRightPointShape={tlstate.onRightPointShape}
|
||||
onDragShape={tlstate.onDragShape}
|
||||
onHoverShape={tlstate.onHoverShape}
|
||||
onUnhoverShape={tlstate.onUnhoverShape}
|
||||
onReleaseShape={tlstate.onReleaseShape}
|
||||
onPointBounds={tlstate.onPointBounds}
|
||||
onDoubleClickBounds={tlstate.onDoubleClickBounds}
|
||||
onRightPointBounds={tlstate.onRightPointBounds}
|
||||
onDragBounds={tlstate.onDragBounds}
|
||||
onHoverBounds={tlstate.onHoverBounds}
|
||||
onUnhoverBounds={tlstate.onUnhoverBounds}
|
||||
onReleaseBounds={tlstate.onReleaseBounds}
|
||||
onPointBoundsHandle={tlstate.onPointBoundsHandle}
|
||||
onDoubleClickBoundsHandle={tlstate.onDoubleClickBoundsHandle}
|
||||
onRightPointBoundsHandle={tlstate.onRightPointBoundsHandle}
|
||||
onDragBoundsHandle={tlstate.onDragBoundsHandle}
|
||||
onHoverBoundsHandle={tlstate.onHoverBoundsHandle}
|
||||
onUnhoverBoundsHandle={tlstate.onUnhoverBoundsHandle}
|
||||
onReleaseBoundsHandle={tlstate.onReleaseBoundsHandle}
|
||||
onPointHandle={tlstate.onPointHandle}
|
||||
onDoubleClickHandle={tlstate.onDoubleClickHandle}
|
||||
onRightPointHandle={tlstate.onRightPointHandle}
|
||||
onDragHandle={tlstate.onDragHandle}
|
||||
onHoverHandle={tlstate.onHoverHandle}
|
||||
onUnhoverHandle={tlstate.onUnhoverHandle}
|
||||
onReleaseHandle={tlstate.onReleaseHandle}
|
||||
onError={tlstate.onError}
|
||||
onRenderCountChange={tlstate.onRenderCountChange}
|
||||
onShapeChange={tlstate.onShapeChange}
|
||||
onShapeBlur={tlstate.onShapeBlur}
|
||||
onShapeClone={tlstate.onShapeClone}
|
||||
onBoundsChange={tlstate.updateBounds}
|
||||
onKeyDown={tlstate.onKeyDown}
|
||||
onKeyUp={tlstate.onKeyUp}
|
||||
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}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{showUI && (
|
||||
<StyledUI>
|
||||
{settings.isFocusMode ? (
|
||||
<FocusButton onSelect={tlstate.toggleFocusMode} />
|
||||
<FocusButton onSelect={state.toggleFocusMode} />
|
||||
) : (
|
||||
<>
|
||||
<TopPanel
|
||||
|
@ -422,7 +454,7 @@ function InnerTldraw({
|
|||
)}
|
||||
</StyledLayout>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const OneOff = React.memo(function OneOff({
|
||||
focusableRef,
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import { styled } from '~styles'
|
||||
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { Data, AlignType, DistributeType, StretchType } from '~types'
|
||||
import { TLDrawSnapshot, 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: Data) => {
|
||||
const has1SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
||||
}
|
||||
const has2SelectedIdsSelector = (s: Data) => {
|
||||
const has2SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
|
||||
}
|
||||
const has3SelectedIdsSelector = (s: Data) => {
|
||||
const has3SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
||||
}
|
||||
|
||||
const isDebugModeSelector = (s: Data) => {
|
||||
const isDebugModeSelector = (s: TLDrawSnapshot) => {
|
||||
return s.settings.isDebugMode
|
||||
}
|
||||
|
||||
const hasGroupSelectedSelector = (s: Data) => {
|
||||
const hasGroupSelectedSelector = (s: TLDrawSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
||||
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
||||
)
|
||||
|
@ -48,7 +48,7 @@ interface ContextMenuProps {
|
|||
}
|
||||
|
||||
export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const hasSelection = useSelector(has1SelectedIdsSelector)
|
||||
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
||||
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
||||
|
@ -58,64 +58,64 @@ export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
|
|||
const rContent = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleFlipHorizontal = React.useCallback(() => {
|
||||
tlstate.flipHorizontal()
|
||||
}, [tlstate])
|
||||
state.flipHorizontal()
|
||||
}, [state])
|
||||
|
||||
const handleFlipVertical = React.useCallback(() => {
|
||||
tlstate.flipVertical()
|
||||
}, [tlstate])
|
||||
state.flipVertical()
|
||||
}, [state])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicate()
|
||||
}, [tlstate])
|
||||
state.duplicate()
|
||||
}, [state])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
tlstate.group()
|
||||
}, [tlstate])
|
||||
state.group()
|
||||
}, [state])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
tlstate.moveToBack()
|
||||
}, [tlstate])
|
||||
state.moveToBack()
|
||||
}, [state])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
tlstate.moveBackward()
|
||||
}, [tlstate])
|
||||
state.moveBackward()
|
||||
}, [state])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
tlstate.moveForward()
|
||||
}, [tlstate])
|
||||
state.moveForward()
|
||||
}, [state])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
tlstate.moveToFront()
|
||||
}, [tlstate])
|
||||
state.moveToFront()
|
||||
}, [state])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
tlstate.delete()
|
||||
}, [tlstate])
|
||||
state.delete()
|
||||
}, [state])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
tlstate.copyJson()
|
||||
}, [tlstate])
|
||||
state.copyJson()
|
||||
}, [state])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
state.copy()
|
||||
}, [state])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
tlstate.paste()
|
||||
}, [tlstate])
|
||||
state.paste()
|
||||
}, [state])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
tlstate.copySvg()
|
||||
}, [tlstate])
|
||||
state.copySvg()
|
||||
}, [state])
|
||||
|
||||
const handleUndo = React.useCallback(() => {
|
||||
tlstate.undo()
|
||||
}, [tlstate])
|
||||
state.undo()
|
||||
}, [state])
|
||||
|
||||
const handleRedo = React.useCallback(() => {
|
||||
tlstate.redo()
|
||||
}, [tlstate])
|
||||
state.redo()
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
|
@ -207,47 +207,47 @@ function AlignDistributeSubMenu({
|
|||
hasTwoOrMore: boolean
|
||||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
const { state } = useTLDrawContext()
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Top)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Top)
|
||||
}, [state])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.CenterVertical)
|
||||
}, [state])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Bottom)
|
||||
}, [state])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
}, [tlstate])
|
||||
state.stretch(StretchType.Vertical)
|
||||
}, [state])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
}, [tlstate])
|
||||
state.distribute(DistributeType.Vertical)
|
||||
}, [state])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Left)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Left)
|
||||
}, [state])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
}, [state])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Right)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Right)
|
||||
}, [state])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
}, [tlstate])
|
||||
state.stretch(StretchType.Horizontal)
|
||||
}, [state])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
}, [tlstate])
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
|
@ -311,11 +311,11 @@ const StyledGridContent = styled(MenuContent, {
|
|||
|
||||
/* ------------------ Move to Page ------------------ */
|
||||
|
||||
const currentPageIdSelector = (s: Data) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: Data) => s.document.pages
|
||||
const currentPageIdSelector = (s: TLDrawSnapshot) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: TLDrawSnapshot) => s.document.pages
|
||||
|
||||
function MoveToPageMenu(): JSX.Element | null {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const documentPages = useSelector(documentPagesSelector)
|
||||
|
||||
|
@ -334,7 +334,7 @@ function MoveToPageMenu(): JSX.Element | null {
|
|||
<CMRowButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => tlstate.moveToPage(id)}
|
||||
onSelect={() => state.moveToPage(id)}
|
||||
>
|
||||
{name || `Page ${i}`}
|
||||
</CMRowButton>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Tooltip } from '~components/Tooltip/Tooltip'
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { styled } from '~styles'
|
||||
import { AlignType, Data, DistributeType, StretchType } from '~types'
|
||||
import { AlignType, TLDrawSnapshot, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
|
@ -33,22 +33,22 @@ import { TrashIcon } from '~components/icons'
|
|||
import { IconButton } from '~components/IconButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const selectedShapesCountSelector = (s: Data) =>
|
||||
const selectedShapesCountSelector = (s: TLDrawSnapshot) =>
|
||||
s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
||||
|
||||
const isAllLockedSelector = (s: Data) => {
|
||||
const isAllLockedSelector = (s: TLDrawSnapshot) => {
|
||||
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: Data) => {
|
||||
const isAllAspectLockedSelector = (s: TLDrawSnapshot) => {
|
||||
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: Data) => {
|
||||
const isAllGroupedSelector = (s: TLDrawSnapshot) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
|
||||
(id) => page.shapes[id]
|
||||
|
@ -62,18 +62,18 @@ const isAllGroupedSelector = (s: Data) => {
|
|||
)
|
||||
}
|
||||
|
||||
const hasSelectionClickor = (s: Data) => {
|
||||
const hasSelectionClickor = (s: TLDrawSnapshot) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 0
|
||||
}
|
||||
|
||||
const hasMultipleSelectionClickor = (s: Data) => {
|
||||
const hasMultipleSelectionClickor = (s: TLDrawSnapshot) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 1
|
||||
}
|
||||
|
||||
export function ActionButton(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const isAllLocked = useSelector(isAllLockedSelector)
|
||||
|
||||
|
@ -86,84 +86,84 @@ export function ActionButton(): JSX.Element {
|
|||
const hasMultipleSelection = useSelector(hasMultipleSelectionClickor)
|
||||
|
||||
const handleRotate = React.useCallback(() => {
|
||||
tlstate.rotate()
|
||||
}, [tlstate])
|
||||
state.rotate()
|
||||
}, [state])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicate()
|
||||
}, [tlstate])
|
||||
state.duplicate()
|
||||
}, [state])
|
||||
|
||||
const handleToggleLocked = React.useCallback(() => {
|
||||
tlstate.toggleLocked()
|
||||
}, [tlstate])
|
||||
state.toggleLocked()
|
||||
}, [state])
|
||||
|
||||
const handleToggleAspectRatio = React.useCallback(() => {
|
||||
tlstate.toggleAspectRatioLocked()
|
||||
}, [tlstate])
|
||||
state.toggleAspectRatioLocked()
|
||||
}, [state])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
tlstate.group()
|
||||
}, [tlstate])
|
||||
state.group()
|
||||
}, [state])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
tlstate.moveToBack()
|
||||
}, [tlstate])
|
||||
state.moveToBack()
|
||||
}, [state])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
tlstate.moveBackward()
|
||||
}, [tlstate])
|
||||
state.moveBackward()
|
||||
}, [state])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
tlstate.moveForward()
|
||||
}, [tlstate])
|
||||
state.moveForward()
|
||||
}, [state])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
tlstate.moveToFront()
|
||||
}, [tlstate])
|
||||
state.moveToFront()
|
||||
}, [state])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
tlstate.delete()
|
||||
}, [tlstate])
|
||||
state.delete()
|
||||
}, [state])
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Top)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Top)
|
||||
}, [state])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.CenterVertical)
|
||||
}, [state])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Bottom)
|
||||
}, [state])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
}, [tlstate])
|
||||
state.stretch(StretchType.Vertical)
|
||||
}, [state])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
}, [tlstate])
|
||||
state.distribute(DistributeType.Vertical)
|
||||
}, [state])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Left)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Left)
|
||||
}, [state])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
}, [state])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Right)
|
||||
}, [tlstate])
|
||||
state.align(AlignType.Right)
|
||||
}, [state])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
}, [tlstate])
|
||||
state.stretch(StretchType.Horizontal)
|
||||
}, [state])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
}, [tlstate])
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
}, [state])
|
||||
|
||||
const selectedShapesCount = useSelector(selectedShapesCountSelector)
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
const isEmptyCanvasSelector = (s: Data) =>
|
||||
const isEmptyCanvasSelector = (s: TLDrawSnapshot) =>
|
||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
||||
s.appState.isEmptyCanvas
|
||||
|
||||
export const BackToContent = React.memo(function BackToContent() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const isEmptyCanvas = useSelector(isEmptyCanvasSelector)
|
||||
|
||||
|
@ -18,7 +18,7 @@ export const BackToContent = React.memo(function BackToContent() {
|
|||
|
||||
return (
|
||||
<BackToContentContainer>
|
||||
<RowButton onSelect={tlstate.zoomToContent}>Back to content</RowButton>
|
||||
<RowButton onSelect={state.zoomToContent}>Back to content</RowButton>
|
||||
</BackToContentContainer>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -3,18 +3,18 @@ import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
|
|||
import { Tooltip } from '~components/Tooltip'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
|
||||
const isToolLockedSelector = (s: Data) => s.appState.isToolLocked
|
||||
const isToolLockedSelector = (s: TLDrawSnapshot) => s.appState.isToolLocked
|
||||
|
||||
export function LockButton(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const isToolLocked = useSelector(isToolLockedSelector)
|
||||
|
||||
return (
|
||||
<Tooltip label="Lock Tool" kbd="7">
|
||||
<ToolButton variant="circle" isActive={isToolLocked} onSelect={tlstate.toggleToolLock}>
|
||||
<ToolButton variant="circle" isActive={isToolLocked} onSelect={state.toggleToolLock}>
|
||||
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
|
|
|
@ -8,45 +8,45 @@ import {
|
|||
SquareIcon,
|
||||
TextIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { Data, TLDrawShapeType } from '~types'
|
||||
import { TLDrawSnapshot, TLDrawShapeType } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { ToolButtonWithTooltip } from '~components/ToolButton'
|
||||
import { Panel } from '~components/Panel'
|
||||
|
||||
const activeToolSelector = (s: Data) => s.appState.activeTool
|
||||
const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool
|
||||
|
||||
export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const activeTool = useSelector(activeToolSelector)
|
||||
|
||||
const selectSelectTool = React.useCallback(() => {
|
||||
tlstate.selectTool('select')
|
||||
}, [tlstate])
|
||||
state.selectTool('select')
|
||||
}, [state])
|
||||
|
||||
const selectDrawTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Draw)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Draw)
|
||||
}, [state])
|
||||
|
||||
const selectRectangleTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Rectangle)
|
||||
}, [state])
|
||||
|
||||
const selectEllipseTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Ellipse)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Ellipse)
|
||||
}, [state])
|
||||
|
||||
const selectArrowTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Arrow)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Arrow)
|
||||
}, [state])
|
||||
|
||||
const selectTextTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Text)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Text)
|
||||
}, [state])
|
||||
|
||||
const selectStickyTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Sticky)
|
||||
}, [tlstate])
|
||||
state.selectTool(TLDrawShapeType.Sticky)
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<Panel side="center">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { styled } from '~styles'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const statusSelector = (s: Data) => s.appState.status
|
||||
const activeToolSelector = (s: Data) => s.appState.activeTool
|
||||
const statusSelector = (s: TLDrawSnapshot) => s.appState.status
|
||||
const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool
|
||||
|
||||
export function StatusBar(): JSX.Element | null {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { StatusBar } from './StatusBar'
|
||||
import { BackToContent } from './BackToContent'
|
||||
|
@ -8,7 +8,7 @@ import { PrimaryTools } from './PrimaryTools'
|
|||
import { ActionButton } from './ActionButton'
|
||||
import { LockButton } from './LockButton'
|
||||
|
||||
const isDebugModeSelector = (s: Data) => s.settings.isDebugMode
|
||||
const isDebugModeSelector = (s: TLDrawSnapshot) => s.settings.isDebugMode
|
||||
|
||||
export const ToolsPanel = React.memo(function ToolsPanel(): JSX.Element {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
|
|
|
@ -5,14 +5,14 @@ import { useTLDrawContext } from '~hooks'
|
|||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { BoxIcon, CircleIcon } from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import type { Data, ColorStyle } from '~types'
|
||||
import type { TLDrawSnapshot, ColorStyle } from '~types'
|
||||
|
||||
const selectColor = (s: Data) => s.appState.selectedStyle.color
|
||||
const selectColor = (s: TLDrawSnapshot) => s.appState.selectedStyle.color
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
const themeSelector = (data: Data) => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
const themeSelector = (data: TLDrawSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export const ColorMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const theme = useSelector(themeSelector)
|
||||
const color = useSelector(selectColor)
|
||||
|
@ -28,7 +28,7 @@ export const ColorMenu = React.memo(function ColorMenu(): JSX.Element {
|
|||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={color === colorStyle}
|
||||
onClick={() => tlstate.style({ color: colorStyle as ColorStyle })}
|
||||
onClick={() => state.style({ color: colorStyle as ColorStyle })}
|
||||
>
|
||||
<BoxIcon
|
||||
fill={strokes[theme][colorStyle as ColorStyle]}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DashStyle, Data } from '~types'
|
||||
import { DashStyle, TLDrawSnapshot } from '~types'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { DashDashedIcon, DashDottedIcon, DashDrawIcon, DashSolidIcon } from '~components/icons'
|
||||
|
@ -13,12 +13,12 @@ const dashes = {
|
|||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
}
|
||||
|
||||
const selectDash = (s: Data) => s.appState.selectedStyle.dash
|
||||
const selectDash = (s: TLDrawSnapshot) => s.appState.selectedStyle.dash
|
||||
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
|
||||
export const DashMenu = React.memo(function DashMenu(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const dash = useSelector(selectDash)
|
||||
|
||||
|
@ -31,7 +31,7 @@ export const DashMenu = React.memo(function DashMenu(): JSX.Element {
|
|||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={dash === dashStyle}
|
||||
onClick={() => tlstate.style({ dash: dashStyle as DashStyle })}
|
||||
onClick={() => state.style({ dash: dashStyle as DashStyle })}
|
||||
>
|
||||
{dashes[dashStyle as DashStyle]}
|
||||
</ToolButton>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import * as React from 'react'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { BoxIcon, IsFilledIcon } from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled
|
||||
const isFilledSelector = (s: TLDrawSnapshot) => s.appState.selectedStyle.isFilled
|
||||
|
||||
export const FillCheckbox = React.memo(function FillCheckbox(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const isFilled = useSelector(isFilledSelector)
|
||||
|
||||
const handleIsFilledChange = React.useCallback(
|
||||
(isFilled: boolean) => tlstate.style({ isFilled }),
|
||||
[tlstate]
|
||||
(isFilled: boolean) => state.style({ isFilled }),
|
||||
[state]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,53 +12,53 @@ interface MenuProps {
|
|||
}
|
||||
|
||||
export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||
const { tlstate, callbacks } = useTLDrawContext()
|
||||
const { state } = useTLDrawContext()
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
|
||||
const handleSignIn = React.useCallback(() => {
|
||||
callbacks.onSignIn?.(tlstate)
|
||||
}, [tlstate])
|
||||
state.callbacks.onSignIn?.(state)
|
||||
}, [state])
|
||||
|
||||
const handleSignOut = React.useCallback(() => {
|
||||
callbacks.onSignOut?.(tlstate)
|
||||
}, [tlstate])
|
||||
state.callbacks.onSignOut?.(state)
|
||||
}, [state])
|
||||
|
||||
const handleCut = React.useCallback(() => {
|
||||
tlstate.cut()
|
||||
}, [tlstate])
|
||||
state.cut()
|
||||
}, [state])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
state.copy()
|
||||
}, [state])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
tlstate.paste()
|
||||
}, [tlstate])
|
||||
state.paste()
|
||||
}, [state])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
tlstate.copySvg()
|
||||
}, [tlstate])
|
||||
state.copySvg()
|
||||
}, [state])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
tlstate.copyJson()
|
||||
}, [tlstate])
|
||||
state.copyJson()
|
||||
}, [state])
|
||||
|
||||
const handleSelectAll = React.useCallback(() => {
|
||||
tlstate.selectAll()
|
||||
}, [tlstate])
|
||||
state.selectAll()
|
||||
}, [state])
|
||||
|
||||
const handleselectNone = React.useCallback(() => {
|
||||
tlstate.selectNone()
|
||||
}, [tlstate])
|
||||
state.selectNone()
|
||||
}, [state])
|
||||
|
||||
const showFileMenu =
|
||||
callbacks.onNewProject ||
|
||||
callbacks.onOpenProject ||
|
||||
callbacks.onSaveProject ||
|
||||
callbacks.onSaveProjectAs
|
||||
state.callbacks.onNewProject ||
|
||||
state.callbacks.onOpenProject ||
|
||||
state.callbacks.onSaveProject ||
|
||||
state.callbacks.onSaveProjectAs
|
||||
|
||||
const showSignInOutMenu = callbacks.onSignIn || callbacks.onSignOut
|
||||
const showSignInOutMenu = state.callbacks.onSignIn || state.callbacks.onSignOut
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
|
@ -68,22 +68,22 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
<DMContent variant="menu">
|
||||
{showFileMenu && (
|
||||
<DMSubMenu label="File...">
|
||||
{callbacks.onNewProject && (
|
||||
{state.callbacks.onNewProject && (
|
||||
<DMItem onSelect={onNewProject} kbd="#N">
|
||||
New Project
|
||||
</DMItem>
|
||||
)}
|
||||
{callbacks.onOpenProject && (
|
||||
{state.callbacks.onOpenProject && (
|
||||
<DMItem onSelect={onOpenProject} kbd="#O">
|
||||
Open...
|
||||
</DMItem>
|
||||
)}
|
||||
{callbacks.onSaveProject && (
|
||||
{state.callbacks.onSaveProject && (
|
||||
<DMItem onSelect={onSaveProject} kbd="#S">
|
||||
Save
|
||||
</DMItem>
|
||||
)}
|
||||
{callbacks.onSaveProjectAs && (
|
||||
{state.callbacks.onSaveProjectAs && (
|
||||
<DMItem onSelect={onSaveProjectAs} kbd="⇧#S">
|
||||
Save As...
|
||||
</DMItem>
|
||||
|
@ -93,10 +93,10 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
{!readOnly && (
|
||||
<>
|
||||
<DMSubMenu label="Edit...">
|
||||
<DMItem onSelect={tlstate.undo} kbd="#Z">
|
||||
<DMItem onSelect={state.undo} kbd="#Z">
|
||||
Undo
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.redo} kbd="#⇧Z">
|
||||
<DMItem onSelect={state.redo} kbd="#⇧Z">
|
||||
Redo
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
|
@ -127,8 +127,8 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
{showSignInOutMenu && (
|
||||
<>
|
||||
<DMDivider dir="ltr" />{' '}
|
||||
{callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
||||
{callbacks.onSignOut && (
|
||||
{state.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
||||
{state.callbacks.onSignOut && (
|
||||
<DMItem onSelect={handleSignOut}>
|
||||
Sign Out
|
||||
<SmallIcon>
|
||||
|
|
|
@ -4,18 +4,19 @@ import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
|
|||
import { PageOptionsDialog } from './PageOptionsDialog'
|
||||
import { styled } from '~styles'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } 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: Data) =>
|
||||
const sortedSelector = (s: TLDrawSnapshot) =>
|
||||
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
|
||||
const currentPageNameSelector = (s: Data) => s.document.pages[s.appState.currentPageId].name
|
||||
const currentPageNameSelector = (s: TLDrawSnapshot) =>
|
||||
s.document.pages[s.appState.currentPageId].name
|
||||
|
||||
const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id
|
||||
const currentPageIdSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId].id
|
||||
|
||||
export function PageMenu(): JSX.Element {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
|
@ -57,22 +58,22 @@ export function PageMenu(): JSX.Element {
|
|||
}
|
||||
|
||||
function PageMenuContent({ onClose }: { onClose: () => void }) {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const sortedPages = useSelector(sortedSelector)
|
||||
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
|
||||
const handleCreatePage = React.useCallback(() => {
|
||||
tlstate.createPage()
|
||||
}, [tlstate])
|
||||
state.createPage()
|
||||
}, [state])
|
||||
|
||||
const handleChangePage = React.useCallback(
|
||||
(id: string) => {
|
||||
onClose()
|
||||
tlstate.changePage(id)
|
||||
state.changePage(id)
|
||||
},
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-alert-dialog'
|
||||
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
||||
import type { Data, TLDrawPage } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawPage } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
import { styled } from '~styles'
|
||||
|
@ -10,7 +10,7 @@ import { IconButton } from '~components/IconButton/IconButton'
|
|||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const canDeleteSelector = (s: Data) => {
|
||||
const canDeleteSelector = (s: TLDrawSnapshot) => {
|
||||
return Object.keys(s.document.pages).length > 1
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ interface PageOptionsDialogProps {
|
|||
}
|
||||
|
||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
|
@ -30,16 +30,16 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
|||
const rInput = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicatePage(page.id)
|
||||
state.duplicatePage(page.id)
|
||||
onClose?.()
|
||||
}, [tlstate])
|
||||
}, [state])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||
tlstate.deletePage(page.id)
|
||||
state.deletePage(page.id)
|
||||
onClose?.()
|
||||
}
|
||||
}, [tlstate])
|
||||
}, [state])
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
|
@ -50,7 +50,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
|||
return
|
||||
}
|
||||
},
|
||||
[tlstate, name]
|
||||
[state, name]
|
||||
)
|
||||
|
||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
|
@ -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)
|
||||
tlstate.renamePage(page.id, nextName || page.name || 'Page')
|
||||
state.renamePage(page.id, nextName || page.name || 'Page')
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
import * as React from 'react'
|
||||
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
|
||||
const settingsSelector = (s: Data) => s.settings
|
||||
const settingsSelector = (s: TLDrawSnapshot) => s.settings
|
||||
|
||||
export function PreferencesMenu() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const settings = useSelector(settingsSelector)
|
||||
|
||||
const toggleDebugMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDebugMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('isDebugMode', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleDarkMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDarkMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('isDarkMode', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleFocusMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isFocusMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('isFocusMode', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleRotateHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleisSnapping = React.useCallback(() => {
|
||||
tlstate.setSetting('isSnapping', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('isSnapping', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
const toggleCloneControls = React.useCallback(() => {
|
||||
tlstate.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
state.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<DMSubMenu label="Preferences">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Data, SizeStyle } from '~types'
|
||||
import { TLDrawSnapshot, SizeStyle } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
@ -12,12 +12,12 @@ const sizes = {
|
|||
[SizeStyle.Large]: <SizeLargeIcon />,
|
||||
}
|
||||
|
||||
const selectSize = (s: Data) => s.appState.selectedStyle.size
|
||||
const selectSize = (s: TLDrawSnapshot) => s.appState.selectedStyle.size
|
||||
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
|
||||
export const SizeMenu = React.memo(function SizeMenu(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const size = useSelector(selectSize)
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const SizeMenu = React.memo(function SizeMenu(): JSX.Element {
|
|||
<ToolButton
|
||||
isActive={size === sizeStyle}
|
||||
variant="icon"
|
||||
onClick={() => tlstate.style({ size: sizeStyle as SizeStyle })}
|
||||
onClick={() => state.style({ size: sizeStyle as SizeStyle })}
|
||||
>
|
||||
{sizes[sizeStyle as SizeStyle]}
|
||||
</ToolButton>
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } 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'
|
||||
|
||||
const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||
const zoomSelector = (s: TLDrawSnapshot) =>
|
||||
s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||
|
||||
export function ZoomMenu() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const zoom = useSelector(zoomSelector)
|
||||
|
||||
return (
|
||||
|
@ -18,19 +19,19 @@ export function ZoomMenu() {
|
|||
<FixedWidthToolButton variant="text">{Math.round(zoom * 100)}%</FixedWidthToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent align="end">
|
||||
<DMItem onSelect={tlstate.zoomIn} kbd="#+">
|
||||
<DMItem onSelect={state.zoomIn} kbd="#+">
|
||||
Zoom In
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomOut} kbd="#−">
|
||||
<DMItem onSelect={state.zoomOut} kbd="#−">
|
||||
Zoom Out
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.resetZoom} kbd="⇧0">
|
||||
<DMItem onSelect={state.resetZoom} kbd="⇧0">
|
||||
To 100%
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
|
||||
<DMItem onSelect={state.zoomToFit} kbd="⇧1">
|
||||
To Fit
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToSelection} kbd="⇧2">
|
||||
<DMItem onSelect={state.zoomToSelection} kbd="⇧2">
|
||||
To Selection
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
|
|
|
@ -2,40 +2,40 @@ import * as React from 'react'
|
|||
import type { TLDrawState } from '~state'
|
||||
|
||||
export function useFileSystem() {
|
||||
const promptSaveBeforeChange = React.useCallback(async (tlstate: TLDrawState) => {
|
||||
if (tlstate.isDirty) {
|
||||
if (tlstate.fileSystemHandle) {
|
||||
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 tlstate.saveProject()
|
||||
await state.saveProject()
|
||||
}
|
||||
} else {
|
||||
if (window.confirm('Do you want to save your current project?')) {
|
||||
await tlstate.saveProject()
|
||||
await state.saveProject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (tlstate: TLDrawState) => {
|
||||
await promptSaveBeforeChange(tlstate)
|
||||
tlstate.newProject()
|
||||
async (state: TLDrawState) => {
|
||||
await promptSaveBeforeChange(state)
|
||||
state.newProject()
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback((tlstate: TLDrawState) => {
|
||||
tlstate.saveProject()
|
||||
const onSaveProject = React.useCallback((state: TLDrawState) => {
|
||||
state.saveProject()
|
||||
}, [])
|
||||
|
||||
const onSaveProjectAs = React.useCallback((tlstate: TLDrawState) => {
|
||||
tlstate.saveProjectAs()
|
||||
const onSaveProjectAs = React.useCallback((state: TLDrawState) => {
|
||||
state.saveProjectAs()
|
||||
}, [])
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (tlstate: TLDrawState) => {
|
||||
await promptSaveBeforeChange(tlstate)
|
||||
tlstate.openProject()
|
||||
async (state: TLDrawState) => {
|
||||
await promptSaveBeforeChange(state)
|
||||
state.openProject()
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
)
|
||||
|
|
|
@ -2,38 +2,38 @@ import * as React from 'react'
|
|||
import { useTLDrawContext } from '~hooks'
|
||||
|
||||
export function useFileSystemHandlers() {
|
||||
const { tlstate, callbacks } = useTLDrawContext()
|
||||
const { state } = useTLDrawContext()
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (e?: KeyboardEvent) => {
|
||||
if (e && callbacks.onOpenProject) e.preventDefault()
|
||||
callbacks.onNewProject?.(tlstate)
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onNewProject?.(state)
|
||||
},
|
||||
[callbacks]
|
||||
[state]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback(
|
||||
(e?: KeyboardEvent) => {
|
||||
if (e && callbacks.onOpenProject) e.preventDefault()
|
||||
callbacks.onSaveProject?.(tlstate)
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onSaveProject?.(state)
|
||||
},
|
||||
[callbacks]
|
||||
[state]
|
||||
)
|
||||
|
||||
const onSaveProjectAs = React.useCallback(
|
||||
(e?: KeyboardEvent) => {
|
||||
if (e && callbacks.onOpenProject) e.preventDefault()
|
||||
callbacks.onSaveProjectAs?.(tlstate)
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onSaveProjectAs?.(state)
|
||||
},
|
||||
[callbacks]
|
||||
[state]
|
||||
)
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (e?: KeyboardEvent) => {
|
||||
if (e && callbacks.onOpenProject) e.preventDefault()
|
||||
callbacks.onOpenProject?.(tlstate)
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onOpenProject?.(state)
|
||||
},
|
||||
[callbacks]
|
||||
[state]
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { TLDrawShapeType } from '~types'
|
|||
import { useFileSystemHandlers, useTLDrawContext } from '~hooks'
|
||||
|
||||
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
const { state } = useTLDrawContext()
|
||||
|
||||
const canHandleEvent = React.useCallback(() => {
|
||||
const elm = ref.current
|
||||
|
@ -16,63 +16,63 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'v,1',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool('select')
|
||||
if (canHandleEvent()) state.selectTool('select')
|
||||
},
|
||||
[tlstate, ref.current]
|
||||
[state, ref.current]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'd,2',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Draw)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Draw)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'r,3',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Rectangle)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'e,4',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Ellipse)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Ellipse)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'a,5',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Arrow)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Arrow)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
't,6',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Text)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Text)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'n,7',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectTool(TLDrawShapeType.Sticky)
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Sticky)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
/* ---------------------- Misc ---------------------- */
|
||||
|
@ -83,12 +83,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'ctrl+shift+d,command+shift+d',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.toggleDarkMode()
|
||||
state.toggleDarkMode()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Focus Mode
|
||||
|
@ -96,10 +96,10 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+.,command+.',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.toggleFocusMode()
|
||||
if (canHandleEvent()) state.toggleFocusMode()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// File System
|
||||
|
@ -114,7 +114,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+s,command+s',
|
||||
|
@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
|
@ -135,7 +135,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+o,command+o',
|
||||
|
@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Undo Redo
|
||||
|
@ -154,30 +154,30 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'command+z,ctrl+z',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
if (tlstate.session) {
|
||||
tlstate.cancelSession()
|
||||
if (state.session) {
|
||||
state.cancelSession()
|
||||
} else {
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-z,command+shift+z',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
if (tlstate.session) {
|
||||
tlstate.cancelSession()
|
||||
if (state.session) {
|
||||
state.cancelSession()
|
||||
} else {
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Undo Redo
|
||||
|
@ -185,19 +185,19 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+u,ctrl+u',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.undoSelect()
|
||||
if (canHandleEvent()) state.undoSelect()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-u,command+shift+u',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.redoSelect()
|
||||
if (canHandleEvent()) state.redoSelect()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
/* -------------------- Commands -------------------- */
|
||||
|
@ -208,51 +208,51 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'ctrl+=,command+=',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.zoomIn()
|
||||
state.zoomIn()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+-,command+-',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.zoomOut()
|
||||
state.zoomOut()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+1',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.zoomToFit()
|
||||
if (canHandleEvent()) state.zoomToFit()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+2',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.zoomToSelection()
|
||||
if (canHandleEvent()) state.zoomToSelection()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+0',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.resetZoom()
|
||||
if (canHandleEvent()) state.resetZoom()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Duplicate
|
||||
|
@ -261,12 +261,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'ctrl+d,command+d',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.duplicate()
|
||||
state.duplicate()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Flip
|
||||
|
@ -274,19 +274,19 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'shift+h',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.flipHorizontal()
|
||||
if (canHandleEvent()) state.flipHorizontal()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+v',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.flipVertical()
|
||||
if (canHandleEvent()) state.flipVertical()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Cancel
|
||||
|
@ -295,11 +295,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'escape',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.cancel()
|
||||
state.cancel()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Delete
|
||||
|
@ -307,10 +307,10 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'backspace',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.delete()
|
||||
if (canHandleEvent()) state.delete()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Select All
|
||||
|
@ -318,10 +318,10 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+a,ctrl+a',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.selectAll()
|
||||
if (canHandleEvent()) state.selectAll()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Nudge
|
||||
|
@ -329,73 +329,73 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'up',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([0, -1], false)
|
||||
if (canHandleEvent()) state.nudge([0, -1], false)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([1, 0], false)
|
||||
if (canHandleEvent()) state.nudge([1, 0], false)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'down',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([0, 1], false)
|
||||
if (canHandleEvent()) state.nudge([0, 1], false)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([-1, 0], false)
|
||||
if (canHandleEvent()) state.nudge([-1, 0], false)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+up',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([0, -1], true)
|
||||
if (canHandleEvent()) state.nudge([0, -1], true)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+right',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([1, 0], true)
|
||||
if (canHandleEvent()) state.nudge([1, 0], true)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+down',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([0, 1], true)
|
||||
if (canHandleEvent()) state.nudge([0, 1], true)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+left',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.nudge([-1, 0], true)
|
||||
if (canHandleEvent()) state.nudge([-1, 0], true)
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Copy, Cut & Paste
|
||||
|
@ -403,28 +403,28 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+c,ctrl+c',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.copy()
|
||||
if (canHandleEvent()) state.copy()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+x,ctrl+x',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.cut()
|
||||
if (canHandleEvent()) state.cut()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+v,ctrl+v',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.paste()
|
||||
if (canHandleEvent()) state.paste()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Group & Ungroup
|
||||
|
@ -433,24 +433,24 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
'command+g,ctrl+g',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.group()
|
||||
state.group()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+g,ctrl+shift+g',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
tlstate.ungroup()
|
||||
state.ungroup()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
// Move
|
||||
|
@ -458,37 +458,37 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'[',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.moveBackward()
|
||||
if (canHandleEvent()) state.moveBackward()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
']',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.moveForward()
|
||||
if (canHandleEvent()) state.moveForward()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.moveToBack()
|
||||
if (canHandleEvent()) state.moveToBack()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.moveToFront()
|
||||
if (canHandleEvent()) state.moveToFront()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
|
@ -496,12 +496,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
tlstate.resetDocument()
|
||||
state.resetDocument()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
[state]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import type { UseBoundStore } from 'zustand'
|
||||
import type { TLDrawState } from '~state'
|
||||
|
||||
export interface TLDrawContextType {
|
||||
tlstate: TLDrawState
|
||||
useSelector: UseBoundStore<Data>
|
||||
callbacks: {
|
||||
onNewProject?: (tlstate: TLDrawState) => void
|
||||
onSaveProject?: (tlstate: TLDrawState) => void
|
||||
onSaveProjectAs?: (tlstate: TLDrawState) => void
|
||||
onOpenProject?: (tlstate: TLDrawState) => void
|
||||
onSignIn?: (tlstate: TLDrawState) => void
|
||||
onSignOut?: (tlstate: TLDrawState) => void
|
||||
}
|
||||
state: TLDrawState
|
||||
useSelector: UseBoundStore<TLDrawSnapshot>
|
||||
}
|
||||
|
||||
export const TLDrawContext = React.createContext<TLDrawContextType>({} as TLDrawContextType)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import type { Data, Theme } from '~types'
|
||||
import type { TLDrawSnapshot, Theme } from '~types'
|
||||
import { useTLDrawContext } from './useTLDrawContext'
|
||||
|
||||
const themeSelector = (data: Data): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
const themeSelector = (data: TLDrawSnapshot): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export function useTheme() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const theme = useSelector(themeSelector)
|
||||
|
||||
return {
|
||||
theme,
|
||||
toggle: tlstate.toggleDarkMode,
|
||||
toggle: state.toggleDarkMode,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
|
||||
import {
|
||||
Data,
|
||||
TLDrawSnapshot,
|
||||
ShapeStyles,
|
||||
ShapesWithProp,
|
||||
TLDrawShape,
|
||||
|
@ -24,13 +24,13 @@ export class TLDR {
|
|||
return getShapeUtils<T>(shape)
|
||||
}
|
||||
|
||||
static getSelectedShapes(data: Data, pageId: string) {
|
||||
static getSelectedShapes(data: TLDrawSnapshot, pageId: string) {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
||||
return selectedIds.map((id) => page.shapes[id])
|
||||
}
|
||||
|
||||
static screenToWorld(data: Data, point: number[]) {
|
||||
static screenToWorld(data: TLDrawSnapshot, point: number[]) {
|
||||
const camera = TLDR.getPageState(data, data.appState.currentPageId).camera
|
||||
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
||||
}
|
||||
|
@ -39,28 +39,28 @@ export class TLDR {
|
|||
return Utils.clamp(zoom, 0.1, 5)
|
||||
}
|
||||
|
||||
static getPage(data: Data, pageId: string): TLDrawPage {
|
||||
static getPage(data: TLDrawSnapshot, pageId: string): TLDrawPage {
|
||||
return data.document.pages[pageId]
|
||||
}
|
||||
|
||||
static getPageState(data: Data, pageId: string): TLPageState {
|
||||
static getPageState(data: TLDrawSnapshot, pageId: string): TLPageState {
|
||||
return data.document.pageStates[pageId]
|
||||
}
|
||||
|
||||
static getSelectedIds(data: Data, pageId: string): string[] {
|
||||
static getSelectedIds(data: TLDrawSnapshot, pageId: string): string[] {
|
||||
return TLDR.getPageState(data, pageId).selectedIds
|
||||
}
|
||||
|
||||
static getShapes(data: Data, pageId: string): TLDrawShape[] {
|
||||
static getShapes(data: TLDrawSnapshot, pageId: string): TLDrawShape[] {
|
||||
return Object.values(TLDR.getPage(data, pageId).shapes)
|
||||
}
|
||||
|
||||
static getCamera(data: Data, pageId: string): TLPageState['camera'] {
|
||||
static getCamera(data: TLDrawSnapshot, pageId: string): TLPageState['camera'] {
|
||||
return TLDR.getPageState(data, pageId).camera
|
||||
}
|
||||
|
||||
static getShape<T extends TLDrawShape = TLDrawShape>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
shapeId: string,
|
||||
pageId: string
|
||||
): T {
|
||||
|
@ -79,7 +79,7 @@ export class TLDR {
|
|||
return TLDR.getShapeUtils(shape).getRotatedBounds(shape)
|
||||
}
|
||||
|
||||
static getSelectedBounds(data: Data): TLBounds {
|
||||
static getSelectedBounds(data: TLDrawSnapshot): TLBounds {
|
||||
return Utils.getCommonBounds(
|
||||
TLDR.getSelectedShapes(data, data.appState.currentPageId).map((shape) =>
|
||||
TLDR.getShapeUtils(shape).getBounds(shape)
|
||||
|
@ -87,11 +87,11 @@ export class TLDR {
|
|||
)
|
||||
}
|
||||
|
||||
static getParentId(data: Data, id: string, pageId: string) {
|
||||
static getParentId(data: TLDrawSnapshot, id: string, pageId: string) {
|
||||
return TLDR.getShape(data, id, pageId).parentId
|
||||
}
|
||||
|
||||
// static getPointedId(data: Data, id: string, pageId: string): string {
|
||||
// static getPointedId(data: TLDrawSnapshot, 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: Data, id: string, pageId: string): string {
|
||||
// static getDrilledPointedId(data: TLDrawSnapshot, 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: Data, id: string, pageId: string): string {
|
||||
// static getTopParentId(data: TLDrawSnapshot, 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: Data, id: string, pageId: string): string[] {
|
||||
static getDocumentBranch(data: TLDrawSnapshot, id: string, pageId: string): string[] {
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
|
||||
if (shape.children === undefined) return [id]
|
||||
|
@ -142,13 +142,13 @@ export class TLDR {
|
|||
|
||||
// Get a deep array of unproxied shapes and their descendants
|
||||
static getSelectedBranchSnapshot<K>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
pageId: string,
|
||||
fn: (shape: TLDrawShape) => K
|
||||
): ({ id: string } & K)[]
|
||||
static getSelectedBranchSnapshot(data: Data, pageId: string): TLDrawShape[]
|
||||
static getSelectedBranchSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
||||
static getSelectedBranchSnapshot<K>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
): (TLDrawShape | K)[] {
|
||||
|
@ -167,14 +167,14 @@ export class TLDR {
|
|||
}
|
||||
|
||||
// Get a shallow array of unproxied shapes
|
||||
static getSelectedShapeSnapshot(data: Data, pageId: string): TLDrawShape[]
|
||||
static getSelectedShapeSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
||||
static getSelectedShapeSnapshot<K>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
): ({ id: string } & K)[]
|
||||
static getSelectedShapeSnapshot<K>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
): (TLDrawShape | K)[] {
|
||||
|
@ -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: Data, ids: string[], pageId: string): string[] {
|
||||
static getAllEffectedShapeIds(data: TLDrawSnapshot, ids: string[], pageId: string): string[] {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
const visited = new Set(ids)
|
||||
|
@ -236,41 +236,47 @@ export class TLDR {
|
|||
}
|
||||
|
||||
static updateBindings(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
id: string,
|
||||
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
|
||||
afterShapes: Record<string, Partial<TLDrawShape>> = {},
|
||||
pageId: string
|
||||
): Data {
|
||||
): TLDrawSnapshot {
|
||||
const page = { ...TLDR.getPage(data, pageId) }
|
||||
return Object.values(page.bindings)
|
||||
.filter((binding) => binding.fromId === id || binding.toId === id)
|
||||
.reduce((cData, binding) => {
|
||||
.reduce((cTLDrawSnapshot, binding) => {
|
||||
if (!beforeShapes[binding.fromId]) {
|
||||
beforeShapes[binding.fromId] = Utils.deepClone(
|
||||
TLDR.getShape(cData, binding.fromId, pageId)
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
||||
)
|
||||
}
|
||||
|
||||
if (!beforeShapes[binding.toId]) {
|
||||
beforeShapes[binding.toId] = Utils.deepClone(TLDR.getShape(cData, binding.toId, pageId))
|
||||
beforeShapes[binding.toId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
)
|
||||
}
|
||||
|
||||
TLDR.onBindingChange(
|
||||
TLDR.getShape(cData, binding.fromId, pageId),
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId),
|
||||
binding,
|
||||
TLDR.getShape(cData, binding.toId, pageId)
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
)
|
||||
|
||||
afterShapes[binding.fromId] = Utils.deepClone(TLDR.getShape(cData, binding.fromId, pageId))
|
||||
afterShapes[binding.toId] = Utils.deepClone(TLDR.getShape(cData, binding.toId, pageId))
|
||||
afterShapes[binding.fromId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
||||
)
|
||||
afterShapes[binding.toId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
)
|
||||
|
||||
return cData
|
||||
return cTLDrawSnapshot
|
||||
}, data)
|
||||
}
|
||||
|
||||
static getLinkedShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
pageId: string,
|
||||
direction: 'center' | 'left' | 'right',
|
||||
includeArrows = true
|
||||
|
@ -372,7 +378,7 @@ export class TLDR {
|
|||
return Array.from(linkedIds.values())
|
||||
}
|
||||
|
||||
static getChildIndexAbove(data: Data, id: string, pageId: string): number {
|
||||
static getChildIndexAbove(data: TLDrawSnapshot, id: string, pageId: string): number {
|
||||
const page = data.document.pages[pageId]
|
||||
const shape = page.shapes[id]
|
||||
|
||||
|
@ -410,14 +416,14 @@ export class TLDR {
|
|||
}
|
||||
|
||||
static mutateShapes<T extends TLDrawShape>(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
fn: (shape: T, i: number) => Partial<T> | void,
|
||||
pageId: string
|
||||
): {
|
||||
before: Record<string, Partial<T>>
|
||||
after: Record<string, Partial<T>>
|
||||
data: Data
|
||||
data: TLDrawSnapshot
|
||||
} {
|
||||
const beforeShapes: Record<string, Partial<T>> = {}
|
||||
const afterShapes: Record<string, Partial<T>> = {}
|
||||
|
@ -440,8 +446,8 @@ export class TLDR {
|
|||
},
|
||||
},
|
||||
})
|
||||
const dataWithBindingChanges = ids.reduce<Data>((cData, id) => {
|
||||
return TLDR.updateBindings(cData, id, beforeShapes, afterShapes, pageId)
|
||||
const dataWithBindingChanges = ids.reduce<TLDrawSnapshot>((cTLDrawSnapshot, id) => {
|
||||
return TLDR.updateBindings(cTLDrawSnapshot, id, beforeShapes, afterShapes, pageId)
|
||||
}, dataWithMutations)
|
||||
|
||||
return {
|
||||
|
@ -451,7 +457,7 @@ export class TLDR {
|
|||
}
|
||||
}
|
||||
|
||||
static createShapes(data: Data, shapes: TLDrawShape[], pageId: string): TLDrawCommand {
|
||||
static createShapes(data: TLDrawSnapshot, shapes: TLDrawShape[], pageId: string): TLDrawCommand {
|
||||
const before: TLDrawPatch = {
|
||||
document: {
|
||||
pages: {
|
||||
|
@ -515,7 +521,7 @@ export class TLDR {
|
|||
}
|
||||
|
||||
static deleteShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
shapes: TLDrawShape[] | string[],
|
||||
pageId?: string
|
||||
): TLDrawCommand {
|
||||
|
@ -612,7 +618,7 @@ export class TLDR {
|
|||
return { ...shape, ...delta }
|
||||
}
|
||||
|
||||
static onChildrenChange<T extends TLDrawShape>(data: Data, shape: T, pageId: string) {
|
||||
static onChildrenChange<T extends TLDrawShape>(data: TLDrawSnapshot, shape: T, pageId: string) {
|
||||
if (!shape.children) return
|
||||
|
||||
const delta = TLDR.getShapeUtils(shape).onChildrenChange?.(
|
||||
|
@ -717,7 +723,7 @@ export class TLDR {
|
|||
/* Parents */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static updateParents(data: Data, pageId: string, changedShapeIds: string[]): void {
|
||||
static updateParents(data: TLDrawSnapshot, pageId: string, changedShapeIds: string[]): void {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
if (changedShapeIds.length === 0) return
|
||||
|
@ -741,7 +747,7 @@ export class TLDR {
|
|||
TLDR.updateParents(data, pageId, parentToUpdateIds)
|
||||
}
|
||||
|
||||
static getSelectedStyle(data: Data, pageId: string): ShapeStyles | false {
|
||||
static getSelectedStyle(data: TLDrawSnapshot, pageId: string): ShapeStyles | false {
|
||||
const { currentStyle } = data.appState
|
||||
|
||||
const page = data.document.pages[pageId]
|
||||
|
@ -782,23 +788,27 @@ export class TLDR {
|
|||
/* Bindings */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static getBinding(data: Data, id: string, pageId: string): TLDrawBinding {
|
||||
static getBinding(data: TLDrawSnapshot, id: string, pageId: string): TLDrawBinding {
|
||||
return TLDR.getPage(data, pageId).bindings[id]
|
||||
}
|
||||
|
||||
static getBindings(data: Data, pageId: string): TLDrawBinding[] {
|
||||
static getBindings(data: TLDrawSnapshot, pageId: string): TLDrawBinding[] {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
return Object.values(page.bindings)
|
||||
}
|
||||
|
||||
static getBindableShapeIds(data: Data) {
|
||||
static getBindableShapeIds(data: TLDrawSnapshot) {
|
||||
return TLDR.getShapes(data, data.appState.currentPageId)
|
||||
.filter((shape) => TLDR.getShapeUtils(shape).canBind)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
}
|
||||
|
||||
static getBindingsWithShapeIds(data: Data, ids: string[], pageId: string): TLDrawBinding[] {
|
||||
static getBindingsWithShapeIds(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
pageId: string
|
||||
): TLDrawBinding[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
TLDR.getBindings(data, pageId).filter((binding) => {
|
||||
|
@ -808,7 +818,7 @@ export class TLDR {
|
|||
)
|
||||
}
|
||||
|
||||
static getRelatedBindings(data: Data, ids: string[], pageId: string): TLDrawBinding[] {
|
||||
static getRelatedBindings(data: TLDrawSnapshot, ids: string[], pageId: string): TLDrawBinding[] {
|
||||
const changedShapeIds = new Set(ids)
|
||||
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
@ -887,7 +897,7 @@ export class TLDR {
|
|||
/* Groups */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static flattenShape = (data: Data, shape: TLDrawShape): TLDrawShape[] => {
|
||||
static flattenShape = (data: TLDrawSnapshot, shape: TLDrawShape): TLDrawShape[] => {
|
||||
return [
|
||||
shape,
|
||||
...(shape.children ?? [])
|
||||
|
@ -897,13 +907,13 @@ export class TLDR {
|
|||
]
|
||||
}
|
||||
|
||||
static flattenPage = (data: Data, pageId: string): TLDrawShape[] => {
|
||||
static flattenPage = (data: TLDrawSnapshot, pageId: string): TLDrawShape[] => {
|
||||
return Object.values(data.document.pages[pageId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.reduce<TLDrawShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
||||
}
|
||||
|
||||
static getTopChildIndex = (data: Data, pageId: string): number => {
|
||||
static getTopChildIndex = (data: TLDrawSnapshot, pageId: string): number => {
|
||||
const shapes = TLDR.getShapes(data, pageId)
|
||||
return shapes.length === 0
|
||||
? 1
|
||||
|
|
|
@ -5,63 +5,63 @@ import { ArrowShape, ColorStyle, SessionType, TLDrawShapeType } from '~types'
|
|||
import type { SelectTool } from './tools/SelectTool'
|
||||
|
||||
describe('TLDrawState', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
state.loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
})
|
||||
|
||||
it('pastes a shape', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const prevCount = Object.keys(tlstate.page.shapes).length
|
||||
const prevCount = Object.keys(state.page.shapes).length
|
||||
|
||||
tlstate.selectNone().copy(['rect1']).paste()
|
||||
state.selectNone().copy(['rect1']).paste()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1)
|
||||
})
|
||||
|
||||
it('pastes a shape to a new page', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
tlstate.selectNone().copy(['rect1']).createPage().paste()
|
||||
state.selectNone().copy(['rect1']).createPage().paste()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(1)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(0)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(0)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(1)
|
||||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.copy()
|
||||
|
||||
const beforeShapes = tlstate.shapes
|
||||
const beforeShapes = state.shapes
|
||||
|
||||
tlstate.paste()
|
||||
state.paste()
|
||||
|
||||
expect(tlstate.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(2)
|
||||
expect(state.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(2)
|
||||
|
||||
const afterShapes = tlstate.shapes
|
||||
const afterShapes = state.shapes
|
||||
|
||||
const newShapes = afterShapes.filter(
|
||||
(shape) => !beforeShapes.find(({ id }) => id === shape.id)
|
||||
|
@ -83,9 +83,9 @@ describe('TLDrawState', () => {
|
|||
|
||||
describe('When copying and pasting a shape with bindings', () => {
|
||||
it('copies two bound shapes and their binding', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
state
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
|
@ -95,23 +95,23 @@ describe('TLDrawState', () => {
|
|||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
expect(state.bindings.length).toBe(1)
|
||||
|
||||
tlstate.selectAll().copy().paste()
|
||||
state.selectAll().copy().paste()
|
||||
|
||||
const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).not.toBe(
|
||||
tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
state.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
)
|
||||
|
||||
expect(tlstate.bindings.length).toBe(2)
|
||||
expect(state.bindings.length).toBe(2)
|
||||
})
|
||||
|
||||
it('removes bindings from copied shape handles', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
state
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
|
@ -121,13 +121,13 @@ describe('TLDrawState', () => {
|
|||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
expect(state.bindings.length).toBe(1)
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
|
||||
|
||||
tlstate.select('arrow1').copy().paste()
|
||||
state.select('arrow1').copy().paste()
|
||||
|
||||
const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).toBeUndefined()
|
||||
})
|
||||
|
@ -135,62 +135,62 @@ describe('TLDrawState', () => {
|
|||
|
||||
describe('Selection', () => {
|
||||
it('selects a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects and deselects a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickCanvas()
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('shift-selects to deselect shapes', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('clears selection when clicking bounds', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlstate.startSession(SessionType.Brush, [-10, -10])
|
||||
tlstate.updateSession([110, 110])
|
||||
tlstate.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(3)
|
||||
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', () => {
|
||||
tlstate.loadDocument(mockDocument).selectAll()
|
||||
state.loadDocument(mockDocument).selectAll()
|
||||
tlu.clickShape('rect2')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
})
|
||||
|
||||
// it('selects shape when double-clicked', () => {
|
||||
// tlstate.loadDocument(mockDocument).selectAll()
|
||||
// state.loadDocument(mockDocument).selectAll()
|
||||
// tlu.doubleClickShape('rect2')
|
||||
// expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
// expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
// })
|
||||
|
||||
it('does not select on meta-click', () => {
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1', { ctrlKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it.todo('deletes shapes if cancelled during creating')
|
||||
|
@ -201,120 +201,120 @@ describe('TLDrawState', () => {
|
|||
|
||||
describe('When selecting all', () => {
|
||||
it('selects all', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(mockDocument).selectAll()
|
||||
expect(tlstate.selectedIds).toMatchSnapshot('selected all')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).selectAll()
|
||||
expect(state.selectedIds).toMatchSnapshot('selected all')
|
||||
})
|
||||
|
||||
it('does not select children of a group', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(mockDocument).selectAll().group()
|
||||
expect(tlstate.selectedIds.length).toBe(1)
|
||||
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', () => {
|
||||
tlstate.selectNone()
|
||||
state.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('single-selects shape in selection on pointerup only', () => {
|
||||
tlstate.selectNone()
|
||||
state.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.pointShape('rect2')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
tlu.stopPointing('rect2')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
// it('selects shapes if shift key is lifted before pointerup', () => {
|
||||
// tlstate.selectNone()
|
||||
// state.selectNone()
|
||||
// tlu.clickShape('rect1')
|
||||
// tlu.pointShape('rect2', { shiftKey: true })
|
||||
// expect(tlstate.appState.status).toBe('pointingBounds')
|
||||
// expect(state.appState.status).toBe('pointingBounds')
|
||||
// tlu.stopPointing('rect2')
|
||||
// expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
// expect(tlstate.appState.status).toBe('idle')
|
||||
// expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
// expect(state.appState.status).toBe('idle')
|
||||
// })
|
||||
})
|
||||
|
||||
describe('Select history', () => {
|
||||
it('selects, undoes and redoes', () => {
|
||||
tlstate.reset().loadDocument(mockDocument)
|
||||
state.reset().loadDocument(mockDocument)
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(0)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
tlu.pointShape('rect1')
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(1)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.stopPointing('rect1')
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(1)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(2)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.selectHistory.pointer).toBe(2)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
|
||||
tlstate.undoSelect()
|
||||
state.undoSelect()
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(1)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlstate.undoSelect()
|
||||
state.undoSelect()
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(0)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
tlstate.redoSelect()
|
||||
state.redoSelect()
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(1)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlstate.select('rect2')
|
||||
state.select('rect2')
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(2)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']])
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.selectHistory.pointer).toBe(2)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
|
||||
tlstate.delete()
|
||||
state.delete()
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(0)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
tlstate.undoSelect()
|
||||
state.undoSelect()
|
||||
|
||||
expect(tlstate.selectHistory.pointer).toBe(0)
|
||||
expect(tlstate.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Copies to JSON', () => {
|
||||
tlstate.selectAll()
|
||||
expect(tlstate.copyJson()).toMatchSnapshot('copied json')
|
||||
state.selectAll()
|
||||
expect(state.copyJson()).toMatchSnapshot('copied json')
|
||||
})
|
||||
|
||||
describe('Mutates bound shapes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect',
|
||||
|
@ -337,88 +337,80 @@ describe('TLDrawState', () => {
|
|||
.selectAll()
|
||||
.style({ color: ColorStyle.Red })
|
||||
|
||||
expect(tlstate.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(tlstate.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
expect(state.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(state.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('arrow').style.color).toBe(ColorStyle.Black)
|
||||
expect(tlstate.getShape('rect').style.color).toBe(ColorStyle.Black)
|
||||
expect(state.getShape('arrow').style.color).toBe(ColorStyle.Black)
|
||||
expect(state.getShape('rect').style.color).toBe(ColorStyle.Black)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(tlstate.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
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 tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.clickShape('rect1')
|
||||
expect((tlstate.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['groupA'])
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(state.selectedIds).toStrictEqual(['groupA'])
|
||||
})
|
||||
|
||||
it('selects the grouped shape when double clicked', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.doubleClickShape('rect1')
|
||||
expect((tlstate.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
})
|
||||
|
||||
it('clears the selectedGroupId when selecting a different shape', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.doubleClickShape('rect1')
|
||||
tlu.clickShape('rect3')
|
||||
expect((tlstate.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect3'])
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(state.selectedIds).toStrictEqual(['rect3'])
|
||||
})
|
||||
|
||||
it('selects a grouped shape when meta-shift-clicked', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.selectNone()
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
|
||||
it('selects a hovered shape from the selected group when meta-shift-clicked', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when creating shapes', () => {
|
||||
it('Creates shapes with the correct child index', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
|
@ -438,39 +430,39 @@ describe('TLDrawState', () => {
|
|||
)
|
||||
.selectTool(TLDrawShapeType.Rectangle)
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
const prevA = tlstate.shapes.map((shape) => shape.id)
|
||||
const prevA = state.shapes.map((shape) => shape.id)
|
||||
|
||||
tlu.pointCanvas({ x: 0, y: 0 })
|
||||
tlu.movePointer({ x: 100, y: 100 })
|
||||
tlu.stopPointing()
|
||||
|
||||
const newIdA = tlstate.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))!
|
||||
const shapeA = tlstate.getShape(newIdA)
|
||||
const newIdA = state.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))!
|
||||
const shapeA = state.getShape(newIdA)
|
||||
expect(shapeA.childIndex).toBe(4)
|
||||
|
||||
tlstate.group(['rect2', 'rect3', newIdA], 'groupA')
|
||||
state.group(['rect2', 'rect3', newIdA], 'groupA')
|
||||
|
||||
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
||||
expect(state.getShape('groupA').childIndex).toBe(2)
|
||||
|
||||
tlstate.selectNone()
|
||||
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||
state.selectNone()
|
||||
state.selectTool(TLDrawShapeType.Rectangle)
|
||||
|
||||
const prevB = tlstate.shapes.map((shape) => shape.id)
|
||||
const prevB = state.shapes.map((shape) => shape.id)
|
||||
|
||||
tlu.pointCanvas({ x: 0, y: 0 })
|
||||
tlu.movePointer({ x: 100, y: 100 })
|
||||
tlu.stopPointing()
|
||||
|
||||
const newIdB = tlstate.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))!
|
||||
const shapeB = tlstate.getShape(newIdB)
|
||||
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 tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
|
@ -485,23 +477,23 @@ describe('TLDrawState', () => {
|
|||
size: [100, 200],
|
||||
})
|
||||
|
||||
expect(tlstate.history.length).toBe(2)
|
||||
expect(state.history.length).toBe(2)
|
||||
|
||||
expect(tlstate.history).toBeDefined()
|
||||
expect(tlstate.history).toMatchSnapshot('history')
|
||||
expect(state.history).toBeDefined()
|
||||
expect(state.history).toMatchSnapshot('history')
|
||||
|
||||
tlstate.history = []
|
||||
expect(tlstate.history).toEqual([])
|
||||
state.history = []
|
||||
expect(state.history).toEqual([])
|
||||
|
||||
const before = tlstate.state
|
||||
tlstate.undo()
|
||||
const after = tlstate.state
|
||||
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 tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
|
@ -517,11 +509,11 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
.undo()
|
||||
|
||||
expect(tlstate.history.length).toBe(1)
|
||||
expect(state.history.length).toBe(1)
|
||||
})
|
||||
|
||||
it('Sets the undo/redo history', () => {
|
||||
const tlstate = new TLDrawState('some_state_a')
|
||||
const state = new TLDrawState('some_state_a')
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -536,29 +528,29 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
// Save the history and document from the first state
|
||||
const doc = tlstate.document
|
||||
const history = tlstate.history
|
||||
const doc = state.document
|
||||
const history = state.history
|
||||
|
||||
// Create a new state
|
||||
const tlstate2 = new TLDrawState('some_state_b')
|
||||
const state2 = new TLDrawState('some_state_b')
|
||||
|
||||
// Load the document and set the history
|
||||
tlstate2.loadDocument(doc)
|
||||
tlstate2.history = history
|
||||
state2.loadDocument(doc)
|
||||
state2.history = history
|
||||
|
||||
expect(tlstate2.shapes.length).toBe(2)
|
||||
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
|
||||
tlstate2.undo()
|
||||
state2.undo()
|
||||
|
||||
expect(tlstate2.shapes.length).toBe(1)
|
||||
expect(state2.shapes.length).toBe(1)
|
||||
})
|
||||
|
||||
describe('When copying to SVG', () => {
|
||||
it('Copies shapes.', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const result = tlstate
|
||||
const state = new TLDrawState()
|
||||
const result = state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.rotate(0.1)
|
||||
|
@ -568,8 +560,8 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const result = tlstate
|
||||
const state = new TLDrawState()
|
||||
const result = state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group()
|
||||
|
@ -581,9 +573,9 @@ describe('TLDrawState', () => {
|
|||
|
||||
it.todo('Copies Text shapes as <text> elements.')
|
||||
// it('Copies Text shapes as <text> elements.', () => {
|
||||
// const tlstate2 = new TLDrawState()
|
||||
// const state2 = new TLDrawState()
|
||||
|
||||
// const svgString = tlstate2
|
||||
// const svgString = state2
|
||||
// .createShapes({
|
||||
// id: 'text1',
|
||||
// type: TLDrawShapeType.Text,
|
||||
|
@ -633,9 +625,9 @@ describe('TLDrawState', () => {
|
|||
|
||||
TLDrawState.defaultState = withoutRoom
|
||||
|
||||
const tlstate = new TLDrawState('migrate_1')
|
||||
const state = new TLDrawState('migrate_1')
|
||||
|
||||
tlstate.createShapes({
|
||||
state.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
})
|
||||
|
@ -645,11 +637,11 @@ describe('TLDrawState', () => {
|
|||
TLDrawState.version = 100
|
||||
TLDrawState.defaultState.room = defaultState.room
|
||||
|
||||
const tlstate2 = new TLDrawState('migrate_1')
|
||||
const state2 = new TLDrawState('migrate_1')
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
expect(tlstate2.getShape('rect1')).toBeTruthy()
|
||||
expect(state2.getShape('rect1')).toBeTruthy()
|
||||
done()
|
||||
} catch (e) {
|
||||
done(e)
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
ShapeStyles,
|
||||
TLDrawShape,
|
||||
TLDrawShapeType,
|
||||
Data,
|
||||
TLDrawSnapshot,
|
||||
Session,
|
||||
TLDrawStatus,
|
||||
SelectHistory,
|
||||
|
@ -58,35 +58,86 @@ import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants'
|
|||
|
||||
const uuid = Utils.uniqueId()
|
||||
|
||||
export class TLDrawState extends StateManager<Data> {
|
||||
private _onMount?: (tlstate: TLDrawState) => void
|
||||
private _onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
||||
private _onUserChange?: (tlstate: TLDrawState, user: TLDrawUser) => void
|
||||
export interface TLDrawCallbacks {
|
||||
/**
|
||||
* (optional) A callback to run when the component mounts.
|
||||
*/
|
||||
onMount?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: (state: TLDrawState, 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
|
||||
/**
|
||||
* (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
|
||||
/**
|
||||
* (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
|
||||
/**
|
||||
* (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
|
||||
/**
|
||||
* (optional) A callback to run when the user signs in via the menu.
|
||||
*/
|
||||
onSignIn?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user signs out via the menu.
|
||||
*/
|
||||
onSignOut?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project.
|
||||
*/
|
||||
onUserChange?: (state: TLDrawState, user: TLDrawUser) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is patched.
|
||||
*/
|
||||
onPatch?: (state: TLDrawState, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is changed with a command.
|
||||
*/
|
||||
onCommand?: (state: TLDrawState, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is persisted.
|
||||
*/
|
||||
onPersist?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user undos.
|
||||
*/
|
||||
onUndo?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user redos.
|
||||
*/
|
||||
onRedo?: (state: TLDrawState) => void
|
||||
}
|
||||
|
||||
readOnly = false
|
||||
|
||||
inputs?: Inputs
|
||||
export class TLDrawState extends StateManager<TLDrawSnapshot> {
|
||||
public callbacks: TLDrawCallbacks = {}
|
||||
|
||||
selectHistory: SelectHistory = {
|
||||
stack: [[]],
|
||||
pointer: 0,
|
||||
}
|
||||
|
||||
clipboard?: {
|
||||
private clipboard?: {
|
||||
shapes: TLDrawShape[]
|
||||
bindings: TLDrawBinding[]
|
||||
}
|
||||
|
||||
tools = createTools(this)
|
||||
private tools = createTools(this)
|
||||
|
||||
currentTool: BaseTool = this.tools.select
|
||||
|
||||
session?: Session
|
||||
|
||||
isCreating = false
|
||||
private isCreating = false
|
||||
|
||||
// The editor's bounding client rect
|
||||
bounds: TLBounds = {
|
||||
private bounds: TLBounds = {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 640,
|
||||
|
@ -96,7 +147,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
|
||||
// The most recent pointer location
|
||||
pointerPoint: number[] = [0, 0]
|
||||
private pointerPoint: number[] = [0, 0]
|
||||
|
||||
private pasteInfo = {
|
||||
center: [0, 0],
|
||||
|
@ -104,15 +155,11 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
|
||||
fileSystemHandle: FileSystemHandle | null = null
|
||||
|
||||
readOnly = false
|
||||
session?: Session
|
||||
isDirty = false
|
||||
|
||||
constructor(
|
||||
id?: string,
|
||||
onMount?: (tlstate: TLDrawState) => void,
|
||||
onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void,
|
||||
onUserChange?: (tlstate: TLDrawState, user: TLDrawUser) => void
|
||||
) {
|
||||
constructor(id?: string, callbacks = {} as TLDrawCallbacks) {
|
||||
super(TLDrawState.defaultState, id, TLDrawState.version, (prev, next, prevVersion) => {
|
||||
return {
|
||||
...next,
|
||||
|
@ -123,23 +170,18 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
})
|
||||
|
||||
this.callbacks = callbacks
|
||||
}
|
||||
|
||||
/* -------------------- Internal -------------------- */
|
||||
|
||||
protected onReady = () => {
|
||||
this.loadDocument(this.document)
|
||||
this.patchState({ document: migrate(this.document, TLDrawState.version) })
|
||||
|
||||
loadFileHandle().then((fileHandle) => {
|
||||
this.fileSystemHandle = fileHandle
|
||||
})
|
||||
|
||||
this._onChange = onChange
|
||||
this._onMount = onMount
|
||||
this._onUserChange = onUserChange
|
||||
|
||||
this.session = undefined
|
||||
}
|
||||
|
||||
/* -------------------- Internal -------------------- */
|
||||
|
||||
onReady = () => {
|
||||
try {
|
||||
this.patchState({
|
||||
appState: {
|
||||
|
@ -160,8 +202,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
})
|
||||
}
|
||||
|
||||
this.persist()
|
||||
this._onMount?.(this)
|
||||
this.callbacks.onMount?.(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,7 +212,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @protected
|
||||
* @returns The final state
|
||||
*/
|
||||
protected cleanup = (state: Data, prev: Data): Data => {
|
||||
protected cleanup = (state: TLDrawSnapshot, prev: TLDrawSnapshot): TLDrawSnapshot => {
|
||||
const data = { ...state }
|
||||
|
||||
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
||||
|
@ -361,29 +402,58 @@ export class TLDrawState extends StateManager<Data> {
|
|||
return data
|
||||
}
|
||||
|
||||
onPatch = (state: TLDrawSnapshot, id?: string) => {
|
||||
this.callbacks.onPatch?.(this, id)
|
||||
}
|
||||
|
||||
onCommand = (state: TLDrawSnapshot, id?: string) => {
|
||||
this.clearSelectHistory()
|
||||
this.isDirty = true
|
||||
this.callbacks.onCommand?.(this, id)
|
||||
}
|
||||
|
||||
onReplace = () => {
|
||||
this.clearSelectHistory()
|
||||
this.isDirty = false
|
||||
}
|
||||
|
||||
onUndo = () => {
|
||||
Session.cache.selectedIds = [...this.selectedIds]
|
||||
this.callbacks.onUndo?.(this)
|
||||
}
|
||||
|
||||
onRedo = () => {
|
||||
Session.cache.selectedIds = [...this.selectedIds]
|
||||
this.callbacks.onRedo?.(this)
|
||||
}
|
||||
|
||||
onPersist = () => {
|
||||
this.callbacks.onPersist?.(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selection history after each new command, undo or redo.
|
||||
* @param state
|
||||
* @param id
|
||||
*/
|
||||
protected onStateDidChange = (state: Data, id: string): void => {
|
||||
if (!id.startsWith('patch')) {
|
||||
if (!id.startsWith('replace')) {
|
||||
// If we've changed the undo stack, then the file is out of
|
||||
// sync with any saved version on the file system.
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
this.clearSelectHistory()
|
||||
}
|
||||
|
||||
if (id.startsWith('undo') || id.startsWith('redo')) {
|
||||
Session.cache.selectedIds = [...this.selectedIds]
|
||||
}
|
||||
|
||||
this._onChange?.(this, state, id)
|
||||
protected onStateDidChange = (_state: TLDrawSnapshot, id?: string): void => {
|
||||
this.callbacks.onChange?.(this, id)
|
||||
}
|
||||
|
||||
// if (id && !id.startsWith('patch')) {
|
||||
// if (!id.startsWith('replace')) {
|
||||
// // If we've changed the undo stack, then the file is out of
|
||||
// // sync with any saved version on the file system.
|
||||
// this.isDirty = true
|
||||
// }
|
||||
// this.clearSelectHistory()
|
||||
// }
|
||||
// if (id.startsWith('undo') || id.startsWith('redo')) {
|
||||
// Session.cache.selectedIds = [...this.selectedIds]
|
||||
// }
|
||||
// this.onChange?.(this, id)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Set the current status.
|
||||
* @param status The new status to set.
|
||||
|
@ -456,13 +526,16 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* Set a setting.
|
||||
*/
|
||||
setSetting = <T extends keyof Data['settings'], V extends Data['settings'][T]>(
|
||||
setSetting = <
|
||||
T extends keyof TLDrawSnapshot['settings'],
|
||||
V extends TLDrawSnapshot['settings'][T]
|
||||
>(
|
||||
name: T,
|
||||
value: V | ((value: V) => V)
|
||||
): this => {
|
||||
if (this.session) return this
|
||||
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
[name]: typeof value === 'function' ? value(this.state.settings[name] as V) : value,
|
||||
|
@ -470,6 +543,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
},
|
||||
`settings:${name}`
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -477,7 +552,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
*/
|
||||
toggleFocusMode = (): this => {
|
||||
if (this.session) return this
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
isFocusMode: !this.state.settings.isFocusMode,
|
||||
|
@ -485,6 +560,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
},
|
||||
`settings:toggled_focus_mode`
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -492,7 +569,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
*/
|
||||
togglePenMode = (): this => {
|
||||
if (this.session) return this
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
isPenMode: !this.state.settings.isPenMode,
|
||||
|
@ -500,6 +577,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
},
|
||||
`settings:toggled_pen_mode`
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -816,8 +895,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
this.resetHistory()
|
||||
this.clearSelectHistory()
|
||||
this.session = undefined
|
||||
|
||||
return this.replaceState(
|
||||
this.replaceState(
|
||||
{
|
||||
...TLDrawState.defaultState,
|
||||
document: migrate(document, TLDrawState.version),
|
||||
|
@ -828,6 +906,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
},
|
||||
'loaded_document'
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
// Should we move this to the app layer? onSave, onSaveAs, etc?
|
||||
|
@ -912,7 +991,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* Get the current app state.
|
||||
*/
|
||||
getAppState = (): Data['appState'] => {
|
||||
getAppState = (): TLDrawSnapshot['appState'] => {
|
||||
return this.appState
|
||||
}
|
||||
|
||||
|
@ -958,10 +1037,6 @@ export class TLDrawState extends StateManager<Data> {
|
|||
return TLDR.getBounds(this.getShape(id, pageId))
|
||||
}
|
||||
|
||||
greet() {
|
||||
return 'hello'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a binding from a given page.
|
||||
* @param id The binding's id.
|
||||
|
@ -1013,7 +1088,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* The current app state.
|
||||
*/
|
||||
get appState(): Data['appState'] {
|
||||
get appState(): TLDrawSnapshot['appState'] {
|
||||
return this.state.appState
|
||||
}
|
||||
|
||||
|
@ -1598,7 +1673,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @param delta The zoom delta.
|
||||
* @param center The point to zoom toward.
|
||||
*/
|
||||
zoom = Utils.throttle((delta: number, center?: number[]): this => {
|
||||
zoomBy = Utils.throttle((delta: number, center?: number[]): this => {
|
||||
const { zoom } = this.pageState.camera
|
||||
const nextZoom = TLDR.getCameraZoom(zoom - delta * zoom)
|
||||
return this.zoomTo(nextZoom, center)
|
||||
|
@ -1639,7 +1714,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
if (this.state.room) {
|
||||
const { users, userId } = this.state.room
|
||||
this._onUserChange?.(this, {
|
||||
|
||||
this.callbacks.onUserChange?.(this, {
|
||||
...users[userId],
|
||||
selectedIds: nextIds,
|
||||
})
|
||||
|
@ -2034,7 +2110,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* Delete all shapes on the page.
|
||||
*/
|
||||
clear = (): this => {
|
||||
deleteAll = (): this => {
|
||||
this.selectAll()
|
||||
this.delete()
|
||||
return this
|
||||
|
@ -2333,7 +2409,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
onZoom: TLWheelEventHandler = (info, e) => {
|
||||
if (this.state.appState.status !== TLDrawStatus.Idle) return
|
||||
this.zoom(info.delta[2] / 100, info.delta)
|
||||
this.zoomBy(info.delta[2] / 100, info.delta)
|
||||
this.onPointerMove(info, e as unknown as React.PointerEvent)
|
||||
}
|
||||
|
||||
|
@ -2349,7 +2425,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
if (this.state.room) {
|
||||
const { users, userId } = this.state.room
|
||||
|
||||
this._onUserChange?.(this, {
|
||||
this.callbacks.onUserChange?.(this, {
|
||||
...users[userId],
|
||||
point: this.getPagePoint(info.point),
|
||||
})
|
||||
|
@ -2565,7 +2641,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
},
|
||||
}
|
||||
|
||||
static defaultState: Data = {
|
||||
static defaultState: TLDrawSnapshot = {
|
||||
settings: {
|
||||
isPenMode: false,
|
||||
isDarkMode: false,
|
||||
|
|
|
@ -4,14 +4,14 @@ import { mockDocument, TLDrawStateUtils } from '~test'
|
|||
import { AlignType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Align command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.loadDocument(mockDocument).select('rect2')
|
||||
const initialState = tlstate.state
|
||||
tlstate.align(AlignType.Top)
|
||||
const currentState = tlstate.state
|
||||
state.loadDocument(mockDocument).select('rect2')
|
||||
const initialState = state.state
|
||||
state.align(AlignType.Top)
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
|
@ -19,67 +19,67 @@ describe('Align command', () => {
|
|||
|
||||
describe('when multiple shapes are selected', () => {
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
tlstate.selectAll()
|
||||
state.loadDocument(mockDocument)
|
||||
state.selectAll()
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.align(AlignType.Top)
|
||||
state.align(AlignType.Top)
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 100])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 100])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns top', () => {
|
||||
tlstate.align(AlignType.Top)
|
||||
state.align(AlignType.Top)
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns right', () => {
|
||||
tlstate.align(AlignType.Right)
|
||||
state.align(AlignType.Right)
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([100, 0])
|
||||
expect(state.getShape('rect1').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns bottom', () => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
state.align(AlignType.Bottom)
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([0, 100])
|
||||
expect(state.getShape('rect1').point).toEqual([0, 100])
|
||||
})
|
||||
|
||||
it('aligns left', () => {
|
||||
tlstate.align(AlignType.Left)
|
||||
state.align(AlignType.Left)
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([0, 100])
|
||||
expect(state.getShape('rect2').point).toEqual([0, 100])
|
||||
})
|
||||
|
||||
it('aligns center horizontal', () => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([50, 0])
|
||||
expect(tlstate.getShape('rect2').point).toEqual([50, 100])
|
||||
expect(state.getShape('rect1').point).toEqual([50, 0])
|
||||
expect(state.getShape('rect2').point).toEqual([50, 100])
|
||||
})
|
||||
|
||||
it('aligns center vertical', () => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
state.align(AlignType.CenterVertical)
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([0, 50])
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 50])
|
||||
expect(state.getShape('rect1').point).toEqual([0, 50])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 50])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when aligning groups', () => {
|
||||
it('aligns children', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||
{ id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||
|
@ -90,12 +90,12 @@ describe('when aligning groups', () => {
|
|||
.select('rect3', 'rect4')
|
||||
.align(AlignType.CenterVertical)
|
||||
|
||||
const p0 = tlstate.getShape('rect4').point
|
||||
const p1 = tlstate.getShape('rect3').point
|
||||
const p0 = state.getShape('rect4').point
|
||||
const p1 = state.getShape('rect3').point
|
||||
|
||||
tlstate.undo().delete(['rect4']).selectAll().align(AlignType.CenterVertical)
|
||||
state.undo().delete(['rect4']).selectAll().align(AlignType.CenterVertical)
|
||||
|
||||
new TLDrawStateUtils(tlstate).expectShapesToBeAtPoints({
|
||||
new TLDrawStateUtils(state).expectShapesToBeAtPoints({
|
||||
rect1: p0,
|
||||
rect2: Vec.add(p0, [100, 100]),
|
||||
rect3: p1,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { AlignType, TLDrawCommand, TLDrawShapeType } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import Vec from '@tldraw/vec'
|
||||
|
||||
export function alignShapes(data: Data, ids: string[], type: AlignType): TLDrawCommand {
|
||||
export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
|
|
|
@ -2,31 +2,31 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Change page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const initialId = tlstate.page.id
|
||||
const initialId = state.page.id
|
||||
|
||||
tlstate.createPage()
|
||||
state.createPage()
|
||||
|
||||
const nextId = tlstate.page.id
|
||||
const nextId = state.page.id
|
||||
|
||||
tlstate.changePage(initialId)
|
||||
state.changePage(initialId)
|
||||
|
||||
expect(tlstate.page.id).toBe(initialId)
|
||||
expect(state.page.id).toBe(initialId)
|
||||
|
||||
tlstate.changePage(nextId)
|
||||
state.changePage(nextId)
|
||||
|
||||
expect(tlstate.page.id).toBe(nextId)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.page.id).toBe(initialId)
|
||||
expect(state.page.id).toBe(initialId)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.page.id).toBe(nextId)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
|
||||
export function changePage(data: Data, pageId: string): TLDrawCommand {
|
||||
export function changePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand {
|
||||
return {
|
||||
id: 'change_page',
|
||||
before: {
|
||||
|
|
|
@ -2,33 +2,33 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Create page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const initialId = tlstate.page.id
|
||||
const initialPageState = tlstate.pageState
|
||||
const initialId = state.page.id
|
||||
const initialPageState = state.pageState
|
||||
|
||||
tlstate.createPage()
|
||||
state.createPage()
|
||||
|
||||
const nextId = tlstate.page.id
|
||||
const nextPageState = tlstate.pageState
|
||||
const nextId = state.page.id
|
||||
const nextPageState = state.pageState
|
||||
|
||||
expect(Object.keys(tlstate.document.pages).length).toBe(2)
|
||||
expect(tlstate.page.id).toBe(nextId)
|
||||
expect(tlstate.pageState).toEqual(nextPageState)
|
||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(state.pageState).toEqual(nextPageState)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.document.pages).length).toBe(1)
|
||||
expect(tlstate.page.id).toBe(initialId)
|
||||
expect(tlstate.pageState).toEqual(initialPageState)
|
||||
expect(Object.keys(state.document.pages).length).toBe(1)
|
||||
expect(state.page.id).toBe(initialId)
|
||||
expect(state.pageState).toEqual(initialPageState)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.document.pages).length).toBe(2)
|
||||
expect(tlstate.page.id).toBe(nextId)
|
||||
expect(tlstate.pageState).toEqual(nextPageState)
|
||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(state.pageState).toEqual(nextPageState)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import type { Data, TLDrawCommand, TLDrawPage } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand, TLDrawPage } from '~types'
|
||||
import { Utils, TLPageState } from '@tldraw/core'
|
||||
|
||||
export function createPage(data: Data, center: number[], pageId = Utils.uniqueId()): TLDrawCommand {
|
||||
export function createPage(
|
||||
data: TLDrawSnapshot,
|
||||
center: number[],
|
||||
pageId = Utils.uniqueId()
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const topPage = Object.values(data.document.pages).sort(
|
||||
|
|
|
@ -2,36 +2,36 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Create command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is provided', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.create()
|
||||
const initialState = state.state
|
||||
state.create()
|
||||
|
||||
const currentState = tlstate.state
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
const shape = { ...tlstate.getShape('rect1'), id: 'rect4' }
|
||||
tlstate.create([shape])
|
||||
const shape = { ...state.getShape('rect1'), id: 'rect4' }
|
||||
state.create([shape])
|
||||
|
||||
expect(tlstate.getShape('rect4')).toBeTruthy()
|
||||
expect(state.getShape('rect4')).toBeTruthy()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect4')).toBe(undefined)
|
||||
expect(state.getShape('rect4')).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect4')).toBeTruthy()
|
||||
expect(state.getShape('rect4')).toBeTruthy()
|
||||
})
|
||||
|
||||
it.todo('Creates bindings')
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Patch } from 'rko'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { TLDrawShape, Data, TLDrawCommand, TLDrawBinding } from '~types'
|
||||
import type { TLDrawShape, TLDrawSnapshot, TLDrawCommand, TLDrawBinding } from '~types'
|
||||
|
||||
export function createShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
shapes: TLDrawShape[],
|
||||
bindings: TLDrawBinding[] = []
|
||||
): TLDrawCommand {
|
||||
|
|
|
@ -2,40 +2,40 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Delete page', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when there are no pages in the current document', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.resetDocument()
|
||||
const initialState = tlstate.state
|
||||
tlstate.deletePage('page1')
|
||||
const currentState = tlstate.state
|
||||
state.resetDocument()
|
||||
const initialState = state.state
|
||||
state.deletePage('page1')
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
const initialId = tlstate.currentPageId
|
||||
const initialId = state.currentPageId
|
||||
|
||||
tlstate.createPage()
|
||||
state.createPage()
|
||||
|
||||
const nextId = tlstate.currentPageId
|
||||
const nextId = state.currentPageId
|
||||
|
||||
tlstate.deletePage()
|
||||
state.deletePage()
|
||||
|
||||
expect(tlstate.currentPageId).toBe(initialId)
|
||||
expect(state.currentPageId).toBe(initialId)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.currentPageId).toBe(nextId)
|
||||
expect(state.currentPageId).toBe(nextId)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.currentPageId).toBe(initialId)
|
||||
expect(state.currentPageId).toBe(initialId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
|
||||
export function deletePage(data: Data, pageId: string): TLDrawCommand {
|
||||
export function deletePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const pagesArr = Object.values(data.document.pages).sort(
|
||||
|
|
|
@ -3,56 +3,56 @@ import { mockDocument } from '~test'
|
|||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Delete command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.delete()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.delete()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect2')
|
||||
tlstate.delete()
|
||||
state.select('rect2')
|
||||
state.delete()
|
||||
|
||||
expect(tlstate.getShape('rect2')).toBe(undefined)
|
||||
expect(tlstate.getPageState().selectedIds.length).toBe(0)
|
||||
expect(state.getShape('rect2')).toBe(undefined)
|
||||
expect(state.getPageState().selectedIds.length).toBe(0)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect2')).toBeTruthy()
|
||||
expect(tlstate.getPageState().selectedIds.length).toBe(1)
|
||||
expect(state.getShape('rect2')).toBeTruthy()
|
||||
expect(state.getPageState().selectedIds.length).toBe(1)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect2')).toBe(undefined)
|
||||
expect(tlstate.getPageState().selectedIds.length).toBe(0)
|
||||
expect(state.getShape('rect2')).toBe(undefined)
|
||||
expect(state.getPageState().selectedIds.length).toBe(0)
|
||||
})
|
||||
|
||||
it('deletes two shapes', () => {
|
||||
tlstate.selectAll()
|
||||
tlstate.delete()
|
||||
state.selectAll()
|
||||
state.delete()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
||||
expect(tlstate.getShape('rect2')).toBe(undefined)
|
||||
expect(state.getShape('rect1')).toBe(undefined)
|
||||
expect(state.getShape('rect2')).toBe(undefined)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect2')).toBeTruthy()
|
||||
expect(state.getShape('rect1')).toBeTruthy()
|
||||
expect(state.getShape('rect2')).toBeTruthy()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
||||
expect(tlstate.getShape('rect2')).toBe(undefined)
|
||||
expect(state.getShape('rect1')).toBe(undefined)
|
||||
expect(state.getShape('rect2')).toBe(undefined)
|
||||
})
|
||||
|
||||
it('deletes bound shapes, undoes and redoes', () => {
|
||||
|
@ -70,9 +70,9 @@ describe('Delete command', () => {
|
|||
})
|
||||
|
||||
it('deletes bound shapes', () => {
|
||||
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
||||
expect(Object.values(state.page.bindings)[0]).toBe(undefined)
|
||||
|
||||
tlstate
|
||||
state
|
||||
.selectNone()
|
||||
.createShapes({
|
||||
id: 'arrow1',
|
||||
|
@ -83,46 +83,46 @@ describe('Delete command', () => {
|
|||
.updateSession([110, 110])
|
||||
.completeSession()
|
||||
|
||||
const binding = Object.values(tlstate.page.bindings)[0]
|
||||
const binding = Object.values(state.page.bindings)[0]
|
||||
|
||||
expect(binding).toBeTruthy()
|
||||
expect(binding.fromId).toBe('arrow1')
|
||||
expect(binding.toId).toBe('rect3')
|
||||
expect(binding.handleId).toBe('start')
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
|
||||
tlstate.select('rect3').delete()
|
||||
state.select('rect3').delete()
|
||||
|
||||
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
expect(Object.values(state.page.bindings)[0]).toBe(undefined)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.values(tlstate.page.bindings)[0]).toBeTruthy()
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
expect(Object.values(state.page.bindings)[0]).toBeTruthy()
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
expect(Object.values(state.page.bindings)[0]).toBe(undefined)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
})
|
||||
|
||||
describe('when deleting shapes in a group', () => {
|
||||
it('updates the group', () => {
|
||||
tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroup').select('rect1').delete()
|
||||
state.group(['rect1', 'rect2', 'rect3'], 'newGroup').select('rect1').delete()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBeUndefined()
|
||||
expect(tlstate.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3'])
|
||||
expect(state.getShape('rect1')).toBeUndefined()
|
||||
expect(state.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when deleting a group', () => {
|
||||
it('deletes all grouped shapes', () => {
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup').select('newGroup').delete()
|
||||
state.group(['rect1', 'rect2'], 'newGroup').select('newGroup').delete()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2')).toBeUndefined()
|
||||
expect(tlstate.getShape('newGroup')).toBeUndefined()
|
||||
expect(state.getShape('rect1')).toBeUndefined()
|
||||
expect(state.getShape('rect2')).toBeUndefined()
|
||||
expect(state.getShape('newGroup')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { TLDR } from '~state/TLDR'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { removeShapesFromPage } from '../shared/removeShapesFromPage'
|
||||
|
||||
export function deleteShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
pageId = data.appState.currentPageId
|
||||
): TLDrawCommand {
|
||||
|
|
|
@ -4,45 +4,45 @@ import { mockDocument, TLDrawStateUtils } from '~test'
|
|||
import { AlignType, DistributeType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Distribute command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when less than three shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
const initialState = tlstate.state
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
const currentState = tlstate.state
|
||||
state.select('rect1', 'rect2')
|
||||
const initialState = state.state
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.selectAll()
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
state.selectAll()
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
|
||||
expect(tlstate.getShape('rect3').point).toEqual([50, 20])
|
||||
tlstate.undo()
|
||||
expect(tlstate.getShape('rect3').point).toEqual([20, 20])
|
||||
tlstate.redo()
|
||||
expect(tlstate.getShape('rect3').point).toEqual([50, 20])
|
||||
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])
|
||||
})
|
||||
|
||||
it('distributes vertically', () => {
|
||||
tlstate.selectAll()
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
state.selectAll()
|
||||
state.distribute(DistributeType.Vertical)
|
||||
|
||||
expect(tlstate.getShape('rect3').point).toEqual([20, 50])
|
||||
expect(state.getShape('rect3').point).toEqual([20, 50])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when distributing groups', () => {
|
||||
it('distributes children', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||
{ id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||
|
@ -54,12 +54,12 @@ describe('when distributing groups', () => {
|
|||
.select('rect3', 'rect4', 'rect5')
|
||||
.distribute(DistributeType.Vertical)
|
||||
|
||||
const p0 = tlstate.getShape('rect4').point
|
||||
const p1 = tlstate.getShape('rect3').point
|
||||
const p0 = state.getShape('rect4').point
|
||||
const p1 = state.getShape('rect3').point
|
||||
|
||||
tlstate.undo().delete(['rect4']).selectAll().distribute(DistributeType.Vertical)
|
||||
state.undo().delete(['rect4']).selectAll().distribute(DistributeType.Vertical)
|
||||
|
||||
new TLDrawStateUtils(tlstate).expectShapesToBeAtPoints({
|
||||
new TLDrawStateUtils(state).expectShapesToBeAtPoints({
|
||||
rect1: p0,
|
||||
rect2: Vec.add(p0, [100, 100]),
|
||||
rect3: p1,
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { DistributeType, TLDrawShape, Data, TLDrawCommand, TLDrawShapeType } from '~types'
|
||||
import { DistributeType, TLDrawShape, TLDrawSnapshot, TLDrawCommand, TLDrawShapeType } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import Vec from '@tldraw/vec'
|
||||
|
||||
export function distributeShapes(data: Data, ids: string[], type: DistributeType): TLDrawCommand {
|
||||
export function distributeShapes(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
type: DistributeType
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
|
|
|
@ -2,23 +2,23 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Duplicate page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const initialId = tlstate.page.id
|
||||
const initialId = state.page.id
|
||||
|
||||
tlstate.duplicatePage(tlstate.currentPageId)
|
||||
state.duplicatePage(state.currentPageId)
|
||||
|
||||
const nextId = tlstate.page.id
|
||||
const nextId = state.page.id
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.page.id).toBe(initialId)
|
||||
expect(state.page.id).toBe(initialId)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.page.id).toBe(nextId)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
||||
export function duplicatePage(data: Data, center: number[], pageId: string): TLDrawCommand {
|
||||
export function duplicatePage(
|
||||
data: TLDrawSnapshot,
|
||||
center: number[],
|
||||
pageId: string
|
||||
): TLDrawCommand {
|
||||
const newId = Utils.uniqueId()
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
|
|
|
@ -6,38 +6,38 @@ import { mockDocument } from '~test'
|
|||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Duplicate command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.duplicate()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.duplicate()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect1')
|
||||
state.select('rect1')
|
||||
|
||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
|
||||
expect(Object.keys(state.getPage().shapes).length).toBe(3)
|
||||
|
||||
tlstate.duplicate()
|
||||
state.duplicate()
|
||||
|
||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(4)
|
||||
expect(Object.keys(state.getPage().shapes).length).toBe(4)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
|
||||
expect(Object.keys(state.getPage().shapes).length).toBe(3)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(4)
|
||||
expect(Object.keys(state.getPage().shapes).length).toBe(4)
|
||||
})
|
||||
|
||||
describe('when duplicating a shape', () => {
|
||||
|
@ -46,7 +46,7 @@ describe('Duplicate command', () => {
|
|||
|
||||
describe('when duplicating a bound shape', () => {
|
||||
it('removed the binding when the target is not selected', () => {
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'target1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -60,33 +60,33 @@ describe('Duplicate command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
const beforeShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const beforeShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
tlstate
|
||||
state
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const beforeArrow = tlstate.getShape<ArrowShape>('arrow1')
|
||||
const beforeArrow = state.getShape<ArrowShape>('arrow1')
|
||||
|
||||
expect(beforeArrow.handles.start.bindingId).toBeTruthy()
|
||||
|
||||
tlstate.select('arrow1').duplicate()
|
||||
state.select('arrow1').duplicate()
|
||||
|
||||
const afterShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const afterShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
const newShapeIds = afterShapeIds.filter((id) => !beforeShapeIds.includes(id))
|
||||
|
||||
expect(newShapeIds.length).toBe(1)
|
||||
|
||||
const duplicatedArrow = tlstate.getShape<ArrowShape>(newShapeIds[0])
|
||||
const duplicatedArrow = state.getShape<ArrowShape>(newShapeIds[0])
|
||||
|
||||
expect(duplicatedArrow.handles.start.bindingId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('duplicates the binding when the target is selected', () => {
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'target1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -100,76 +100,74 @@ describe('Duplicate command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
const beforeShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const beforeShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
tlstate
|
||||
state
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const oldBindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
const oldBindingId = state.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
expect(oldBindingId).toBeTruthy()
|
||||
|
||||
tlstate.select('arrow1', 'target1').duplicate()
|
||||
state.select('arrow1', 'target1').duplicate()
|
||||
|
||||
const afterShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const afterShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
const newShapeIds = afterShapeIds.filter((id) => !beforeShapeIds.includes(id))
|
||||
|
||||
expect(newShapeIds.length).toBe(2)
|
||||
|
||||
const newBindingId = tlstate.getShape<ArrowShape>(newShapeIds[0]).handles.start.bindingId
|
||||
const newBindingId = state.getShape<ArrowShape>(newShapeIds[0]).handles.start.bindingId
|
||||
|
||||
expect(newBindingId).toBeTruthy()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getBinding(newBindingId!)).toBeUndefined()
|
||||
expect(tlstate.getShape<ArrowShape>(newShapeIds[0])).toBeUndefined()
|
||||
expect(state.getBinding(newBindingId!)).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>(newShapeIds[0])).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getBinding(newBindingId!)).toBeTruthy()
|
||||
expect(tlstate.getShape<ArrowShape>(newShapeIds[0]).handles.start.bindingId).toBe(
|
||||
newBindingId
|
||||
)
|
||||
expect(state.getBinding(newBindingId!)).toBeTruthy()
|
||||
expect(state.getShape<ArrowShape>(newShapeIds[0]).handles.start.bindingId).toBe(newBindingId)
|
||||
})
|
||||
|
||||
it('duplicates groups', () => {
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup').select('newGroup')
|
||||
state.group(['rect1', 'rect2'], 'newGroup').select('newGroup')
|
||||
|
||||
const beforeShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const beforeShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
tlstate.duplicate()
|
||||
state.duplicate()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length + 3)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 3)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length + 3)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 3)
|
||||
})
|
||||
|
||||
it('duplicates grouped shapes', () => {
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup').select('rect1')
|
||||
state.group(['rect1', 'rect2'], 'newGroup').select('rect1')
|
||||
|
||||
const beforeShapeIds = Object.keys(tlstate.page.shapes)
|
||||
const beforeShapeIds = Object.keys(state.page.shapes)
|
||||
|
||||
tlstate.duplicate()
|
||||
state.duplicate()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length + 1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 1)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(beforeShapeIds.length + 1)
|
||||
expect(Object.keys(state.page.shapes).length).toBe(beforeShapeIds.length + 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -178,25 +176,25 @@ describe('Duplicate command', () => {
|
|||
|
||||
describe('when point-duplicating', () => {
|
||||
it('duplicates without crashing', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'])
|
||||
.selectAll()
|
||||
.duplicate(tlstate.selectedIds, [200, 200])
|
||||
.duplicate(state.selectedIds, [200, 200])
|
||||
})
|
||||
|
||||
it('duplicates in the correct place', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate.loadDocument(mockDocument).group(['rect1', 'rect2']).selectAll()
|
||||
state.loadDocument(mockDocument).group(['rect1', 'rect2']).selectAll()
|
||||
|
||||
const before = tlstate.shapes.map((shape) => shape.id)
|
||||
const before = state.shapes.map((shape) => shape.id)
|
||||
|
||||
tlstate.duplicate(tlstate.selectedIds, [200, 200])
|
||||
state.duplicate(state.selectedIds, [200, 200])
|
||||
|
||||
const after = tlstate.shapes.filter((shape) => !before.includes(shape.id))
|
||||
const after = state.shapes.filter((shape) => !before.includes(shape.id))
|
||||
|
||||
expect(
|
||||
Utils.getBoundsCenter(Utils.getCommonBounds(after.map((shape) => TLDR.getBounds(shape))))
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
import { Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||
import type { TLDrawSnapshot, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||
|
||||
export function duplicateShapes(data: Data, ids: string[], point?: number[]): TLDrawCommand {
|
||||
export function duplicateShapes(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
point?: number[]
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const page = TLDR.getPage(data, currentPageId)
|
||||
|
|
|
@ -3,48 +3,48 @@ import { mockDocument } from '~test'
|
|||
import type { RectangleShape } from '~types'
|
||||
|
||||
describe('Flip command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.flipHorizontal()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.flipHorizontal()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.flipHorizontal()
|
||||
state.select('rect1', 'rect2')
|
||||
state.flipHorizontal()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
})
|
||||
|
||||
it('flips horizontally', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.flipHorizontal()
|
||||
state.select('rect1', 'rect2')
|
||||
state.flipHorizontal()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([100, 0])
|
||||
})
|
||||
|
||||
it('flips vertically', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.flipVertical()
|
||||
state.select('rect1', 'rect2')
|
||||
state.flipVertical()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 100])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 100])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FlipType } from '~types'
|
||||
import { TLBoundsCorner, Utils } from '@tldraw/core'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function flipShapes(data: Data, ids: string[], type: FlipType): TLDrawCommand {
|
||||
export function flipShapes(data: TLDrawSnapshot, ids: string[], type: FlipType): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
|
||||
|
|
|
@ -4,42 +4,42 @@ import { mockDocument } from '~test'
|
|||
import { GroupShape, TLDrawShape, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Group command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup')
|
||||
state.group(['rect1', 'rect2'], 'newGroup')
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroup')).toBeTruthy()
|
||||
expect(state.getShape<GroupShape>('newGroup')).toBeTruthy()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroup')).toBeTruthy()
|
||||
expect(state.getShape<GroupShape>('newGroup')).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.selectNone()
|
||||
state.selectNone()
|
||||
|
||||
// @ts-ignore
|
||||
const stackLength = tlstate.stack.length
|
||||
const stackLength = state.stack.length
|
||||
|
||||
tlstate.group([], 'newGroup')
|
||||
expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
state.group([], 'newGroup')
|
||||
expect(state.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
// @ts-ignore
|
||||
expect(tlstate.stack.length).toBe(stackLength)
|
||||
expect(state.stack.length).toBe(stackLength)
|
||||
|
||||
tlstate.group(['rect1'], 'newGroup')
|
||||
expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
state.group(['rect1'], 'newGroup')
|
||||
expect(state.getShape<GroupShape>('newGroup')).toBeUndefined()
|
||||
// @ts-ignore
|
||||
expect(tlstate.stack.length).toBe(stackLength)
|
||||
expect(state.stack.length).toBe(stackLength)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('Group command', () => {
|
|||
*/
|
||||
|
||||
it('creates a group with the correct props', () => {
|
||||
tlstate.updateShapes(
|
||||
state.updateShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
point: [300, 300],
|
||||
|
@ -64,8 +64,8 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup')
|
||||
const group = tlstate.getShape<GroupShape>('newGroup')
|
||||
state.group(['rect1', 'rect2'], 'newGroup')
|
||||
const group = state.getShape<GroupShape>('newGroup')
|
||||
expect(group).toBeTruthy()
|
||||
expect(group.parentId).toBe('page1')
|
||||
expect(group.childIndex).toBe(3)
|
||||
|
@ -74,7 +74,7 @@ describe('Group command', () => {
|
|||
})
|
||||
|
||||
it('reparents the grouped shapes', () => {
|
||||
tlstate.updateShapes(
|
||||
state.updateShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
childIndex: 2.5,
|
||||
|
@ -85,13 +85,13 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroup')
|
||||
state.group(['rect1', 'rect2'], 'newGroup')
|
||||
|
||||
let rect1: TLDrawShape
|
||||
let rect2: TLDrawShape
|
||||
|
||||
rect1 = tlstate.getShape('rect1')
|
||||
rect2 = tlstate.getShape('rect2')
|
||||
rect1 = state.getShape('rect1')
|
||||
rect2 = state.getShape('rect2')
|
||||
// Reparents the shapes
|
||||
expect(rect1.parentId).toBe('newGroup')
|
||||
expect(rect2.parentId).toBe('newGroup')
|
||||
|
@ -99,10 +99,10 @@ describe('Group command', () => {
|
|||
expect(rect1.childIndex).toBe(1)
|
||||
expect(rect2.childIndex).toBe(2)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
rect1 = tlstate.getShape('rect1')
|
||||
rect2 = tlstate.getShape('rect2')
|
||||
rect1 = state.getShape('rect1')
|
||||
rect2 = state.getShape('rect2')
|
||||
// Restores the shapes' parentIds
|
||||
expect(rect1.parentId).toBe('page1')
|
||||
expect(rect2.parentId).toBe('page1')
|
||||
|
@ -127,7 +127,7 @@ describe('Group command', () => {
|
|||
original group be updated to only contain the remaining ones.
|
||||
*/
|
||||
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -150,59 +150,59 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2', 'rect3', 'rect4'], 'newGroupA')
|
||||
state.group(['rect1', 'rect2', 'rect3', 'rect4'], 'newGroupA')
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(3)
|
||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual([
|
||||
expect(state.getShape<GroupShape>('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<GroupShape>('newGroupA').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
'rect4',
|
||||
])
|
||||
|
||||
tlstate.group(['rect1', 'rect3'], 'newGroupB')
|
||||
state.group(['rect1', 'rect3'], 'newGroupB')
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(state.getShape('rect2').childIndex).toBe(2)
|
||||
expect(state.getShape('rect4').childIndex).toBe(4)
|
||||
expect(state.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(2)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeTruthy()
|
||||
expect(state.getShape('rect1').childIndex).toBe(1)
|
||||
expect(state.getShape('rect3').childIndex).toBe(2)
|
||||
expect(state.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(3)
|
||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual([
|
||||
expect(state.getShape<GroupShape>('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<GroupShape>('newGroupA').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
'rect4',
|
||||
])
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeTruthy()
|
||||
expect(state.getShape('rect2').childIndex).toBe(2)
|
||||
expect(state.getShape('rect4').childIndex).toBe(4)
|
||||
expect(state.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeTruthy()
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(2)
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeTruthy()
|
||||
expect(state.getShape('rect1').childIndex).toBe(1)
|
||||
expect(state.getShape('rect3').childIndex).toBe(2)
|
||||
expect(state.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
|
||||
})
|
||||
|
||||
it('does nothing if all shapes in the group are selected', () => {
|
||||
|
@ -210,7 +210,7 @@ describe('Group command', () => {
|
|||
If the selected shapes represent ALL of the children of the a
|
||||
group, then no effect should occur.
|
||||
*/
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -228,9 +228,9 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupA')
|
||||
tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
state.group(['rect1', 'rect2', 'rect3'], 'newGroupA')
|
||||
state.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('deletes any groups that no longer have children', () => {
|
||||
|
@ -240,7 +240,7 @@ describe('Group command', () => {
|
|||
Other rules around deleted shapes should here apply: bindings
|
||||
connected to the group should be deleted, etc.
|
||||
*/
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -258,10 +258,10 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroupA')
|
||||
tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
state.group(['rect1', 'rect2'], 'newGroupA')
|
||||
state.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
|
@ -274,7 +274,7 @@ 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.
|
||||
*/
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -292,26 +292,26 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
tlstate.group(['rect1', 'rect2'], 'newGroupA')
|
||||
tlstate.group(['newGroupA', 'rect3'], 'newGroupB')
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
state.group(['rect1', 'rect2'], 'newGroupA')
|
||||
state.group(['newGroupA', 'rect3'], 'newGroupB')
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeDefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeDefined()
|
||||
expect(state.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect1', 'rect2'])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB')).toBeDefined()
|
||||
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
expect(state.getShape<GroupShape>('newGroupA')).toBeUndefined()
|
||||
expect(state.getShape<GroupShape>('newGroupB')).toBeDefined()
|
||||
expect(state.getShape<GroupShape>('newGroupB').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
|
@ -319,7 +319,7 @@ describe('Group command', () => {
|
|||
})
|
||||
|
||||
it('Ungroups if the only shape selected is a group', () => {
|
||||
tlstate.resetDocument().createShapes(
|
||||
state.resetDocument().createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -337,15 +337,15 @@ describe('Group command', () => {
|
|||
}
|
||||
)
|
||||
|
||||
expect(tlstate.shapes.length).toBe(3)
|
||||
expect(state.shapes.length).toBe(3)
|
||||
|
||||
tlstate.selectAll().group()
|
||||
state.selectAll().group()
|
||||
|
||||
expect(tlstate.shapes.length).toBe(4)
|
||||
expect(state.shapes.length).toBe(4)
|
||||
|
||||
tlstate.selectAll().group()
|
||||
state.selectAll().group()
|
||||
|
||||
expect(tlstate.shapes.length).toBe(3)
|
||||
expect(state.shapes.length).toBe(3)
|
||||
})
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { TLDrawBinding, TLDrawShape, TLDrawShapeType } from '~types'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { Patch } from 'rko'
|
||||
|
||||
export function groupShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
groupId: string,
|
||||
pageId: string
|
||||
|
|
|
@ -3,17 +3,17 @@ import { mockDocument } from '~test'
|
|||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Move to page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument).createPage('page2').changePage('page1')
|
||||
state.loadDocument(mockDocument).createPage('page2').changePage('page1')
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.moveToPage('page2')
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.moveToPage('page2')
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
|
@ -29,37 +29,37 @@ describe('Move to page command', () => {
|
|||
*/
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect1', 'rect2').moveToPage('page2')
|
||||
state.select('rect1', 'rect2').moveToPage('page2')
|
||||
|
||||
expect(tlstate.currentPageId).toBe('page2')
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
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'])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.currentPageId).toBe('page1')
|
||||
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')
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.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(state.currentPageId).toBe('page2')
|
||||
})
|
||||
|
||||
describe('when moving shapes with bindings', () => {
|
||||
it('deletes bindings when only the bound-to shape is moved', () => {
|
||||
tlstate
|
||||
state
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
|
@ -71,36 +71,30 @@ describe('Move to page command', () => {
|
|||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
const bindingId = state.bindings[0].id
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.select('target1').moveToPage('page2')
|
||||
state.select('target1').moveToPage('page2')
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('deletes bindings when only the bound-from shape is moved', () => {
|
||||
tlstate
|
||||
state
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
|
@ -112,36 +106,30 @@ describe('Move to page command', () => {
|
|||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
const bindingId = state.bindings[0].id
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.select('arrow1').moveToPage('page2')
|
||||
state.select('arrow1').moveToPage('page2')
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('moves bindings when both shapes are moved', () => {
|
||||
tlstate
|
||||
state
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
|
@ -153,87 +141,77 @@ describe('Move to page command', () => {
|
|||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
const bindingId = state.bindings[0].id
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.select('arrow1', 'target1').moveToPage('page2')
|
||||
state.select('arrow1', 'target1').moveToPage('page2')
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(state.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when moving grouped shapes', () => {
|
||||
it('moves groups and their children', () => {
|
||||
tlstate.group(['rect1', 'rect2'], 'groupA').moveToPage('page2')
|
||||
state.group(['rect1', 'rect2'], 'groupA').moveToPage('page2')
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('groupA', 'page1')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||
expect(state.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(state.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(state.getShape('groupA', 'page2')).toBeDefined()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeUndefined()
|
||||
expect(state.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(state.getShape('rect2', 'page2')).toBeUndefined()
|
||||
expect(state.getShape('groupA', 'page2')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeDefined()
|
||||
expect(state.getShape('rect1', 'page1')).toBeDefined()
|
||||
expect(state.getShape('rect2', 'page1')).toBeDefined()
|
||||
expect(state.getShape('groupA', 'page1')).toBeDefined()
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('groupA', 'page1')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||
expect(state.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(state.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(state.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', () => {
|
||||
tlstate.group(['rect1', 'rect2', 'rect3'], 'groupA').select('rect1').moveToPage('page2')
|
||||
state.group(['rect1', 'rect2', 'rect3'], 'groupA').select('rect1').moveToPage('page2')
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect1', 'page2').parentId).toBe('page2')
|
||||
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual(['rect2', 'rect3'])
|
||||
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'])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page1').parentId).toBe('groupA')
|
||||
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
])
|
||||
expect(state.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(state.getShape('rect1', 'page1').parentId).toBe('groupA')
|
||||
expect(state.getShape('groupA', 'page1').children).toStrictEqual(['rect1', 'rect2', 'rect3'])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { ArrowShape, Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||
import type { ArrowShape, TLDrawSnapshot, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { Utils, TLBounds } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
|
||||
export function moveShapesToPage(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
viewportBounds: TLBounds,
|
||||
fromPageId: string,
|
||||
|
|
|
@ -2,24 +2,24 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Rename page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const initialId = tlstate.page.id
|
||||
const initialName = tlstate.page.name
|
||||
const initialId = state.page.id
|
||||
const initialName = state.page.name
|
||||
|
||||
tlstate.renamePage(initialId, 'My Special Page')
|
||||
state.renamePage(initialId, 'My Special Page')
|
||||
|
||||
expect(tlstate.page.name).toBe('My Special Page')
|
||||
expect(state.page.name).toBe('My Special Page')
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.page.name).toBe(initialName)
|
||||
expect(state.page.name).toBe(initialName)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.page.name).toBe('My Special Page')
|
||||
expect(state.page.name).toBe('My Special Page')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
|
||||
export function renamePage(data: Data, pageId: string, name: string): TLDrawCommand {
|
||||
export function renamePage(data: TLDrawSnapshot, pageId: string, name: string): TLDrawCommand {
|
||||
const page = data.document.pages[pageId]
|
||||
return {
|
||||
id: 'rename_page',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { Data, TLDrawShapeType } from '~types'
|
||||
import { TLDrawSnapshot, TLDrawShapeType } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
const tlstate = new TLDrawState().createShapes(
|
||||
const state = new TLDrawState().createShapes(
|
||||
{
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
id: 'a',
|
||||
|
@ -25,16 +25,16 @@ const tlstate = new TLDrawState().createShapes(
|
|||
}
|
||||
)
|
||||
|
||||
const doc = { ...tlstate.document }
|
||||
const doc = { ...state.document }
|
||||
|
||||
function getSortedShapeIds(data: Data) {
|
||||
function getSortedShapeIds(data: TLDrawSnapshot) {
|
||||
return TLDR.getShapes(data, data.appState.currentPageId)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function getSortedIndices(data: Data) {
|
||||
function getSortedIndices(data: TLDrawSnapshot) {
|
||||
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: Data) {
|
|||
|
||||
describe('Move command', () => {
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(doc)
|
||||
state.loadDocument(doc)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.moveToBack()
|
||||
const initialState = state.state
|
||||
state.moveToBack()
|
||||
|
||||
const currentState = tlstate.state
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('b')
|
||||
tlstate.moveToBack()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bacd')
|
||||
tlstate.undo()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
|
||||
tlstate.redo()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bacd')
|
||||
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')
|
||||
})
|
||||
|
||||
describe('to back', () => {
|
||||
it('moves a shape to back', () => {
|
||||
tlstate.select('b')
|
||||
tlstate.moveToBack()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bacd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('0.50,1.00,3.00,4.00')
|
||||
state.select('b')
|
||||
state.moveToBack()
|
||||
expect(getSortedShapeIds(state.state)).toBe('bacd')
|
||||
expect(getSortedIndices(state.state)).toBe('0.50,1.00,3.00,4.00')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to back', () => {
|
||||
tlstate.select('b', 'c')
|
||||
tlstate.moveToBack()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bcad')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('0.33,0.67,1.00,4.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to back', () => {
|
||||
tlstate.select('b', 'd')
|
||||
tlstate.moveToBack()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bdac')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('0.33,0.67,1.00,3.00')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('backward', () => {
|
||||
it('moves a shape backward', () => {
|
||||
tlstate.select('c')
|
||||
tlstate.moveBackward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('acbd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,1.50,2.00,4.00')
|
||||
state.select('c')
|
||||
state.moveBackward()
|
||||
expect(getSortedShapeIds(state.state)).toBe('acbd')
|
||||
expect(getSortedIndices(state.state)).toBe('1.00,1.50,2.00,4.00')
|
||||
})
|
||||
|
||||
it('moves a shape at first index backward', () => {
|
||||
tlstate.select('a')
|
||||
tlstate.moveBackward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,2.00,3.00,4.00')
|
||||
state.select('a')
|
||||
state.moveBackward()
|
||||
expect(getSortedShapeIds(state.state)).toBe('abcd')
|
||||
expect(getSortedIndices(state.state)).toBe('1.00,2.00,3.00,4.00')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward', () => {
|
||||
tlstate.select('c', 'd')
|
||||
tlstate.moveBackward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('acdb')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,1.50,1.67,2.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings backward', () => {
|
||||
tlstate.select('b', 'd')
|
||||
tlstate.moveBackward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('badc')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('0.50,1.00,2.50,3.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward at zero index', () => {
|
||||
tlstate.select('a', 'b')
|
||||
tlstate.moveBackward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,2.00,3.00,4.00')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('forward', () => {
|
||||
it('moves a shape forward', () => {
|
||||
tlstate.select('c')
|
||||
tlstate.moveForward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abdc')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,2.00,4.00,5.00')
|
||||
state.select('c')
|
||||
state.moveForward()
|
||||
expect(getSortedShapeIds(state.state)).toBe('abdc')
|
||||
expect(getSortedIndices(state.state)).toBe('1.00,2.00,4.00,5.00')
|
||||
})
|
||||
|
||||
it('moves a shape forward at the top index', () => {
|
||||
tlstate.select('b')
|
||||
tlstate.moveForward()
|
||||
tlstate.moveForward()
|
||||
tlstate.moveForward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('acdb')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,3.00,4.00,5.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward', () => {
|
||||
tlstate.select('a', 'b')
|
||||
tlstate.moveForward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('cabd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('3.00,3.33,3.50,4.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings forward', () => {
|
||||
tlstate.select('a', 'c')
|
||||
tlstate.moveForward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('badc')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('2.00,2.50,4.00,5.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward at top index', () => {
|
||||
tlstate.select('c', 'd')
|
||||
tlstate.moveForward()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,2.00,3.00,4.00')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('to front', () => {
|
||||
it('moves a shape to front', () => {
|
||||
tlstate.select('b')
|
||||
tlstate.moveToFront()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('acdb')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,3.00,4.00,5.00')
|
||||
state.select('b')
|
||||
state.moveToFront()
|
||||
expect(getSortedShapeIds(state.state)).toBe('acdb')
|
||||
expect(getSortedIndices(state.state)).toBe('1.00,3.00,4.00,5.00')
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to front', () => {
|
||||
tlstate.select('a', 'b')
|
||||
tlstate.moveToFront()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('cdab')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('3.00,4.00,5.00,6.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to front', () => {
|
||||
tlstate.select('a', 'c')
|
||||
tlstate.moveToFront()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('bdac')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('2.00,4.00,5.00,6.00')
|
||||
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')
|
||||
})
|
||||
|
||||
it('moves siblings already at front to front', () => {
|
||||
tlstate.select('c', 'd')
|
||||
tlstate.moveToFront()
|
||||
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
|
||||
expect(getSortedIndices(tlstate.state)).toBe('1.00,2.00,3.00,4.00')
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { MoveType, Data, TLDrawShape, TLDrawCommand } from '~types'
|
||||
import { MoveType, TLDrawSnapshot, TLDrawShape, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function reorderShapes(data: Data, ids: string[], type: MoveType): TLDrawCommand {
|
||||
export function reorderShapes(data: TLDrawSnapshot, ids: string[], type: MoveType): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
// Get the unique parent ids for the selected elements
|
||||
|
|
|
@ -5,14 +5,14 @@ import { mockDocument } from '~test'
|
|||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Reset bounds command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.createShapes({
|
||||
state.createShapes({
|
||||
id: 'text1',
|
||||
type: TLDrawShapeType.Text,
|
||||
point: [0, 0],
|
||||
|
@ -20,18 +20,18 @@ describe('Reset bounds command', () => {
|
|||
})
|
||||
|
||||
// Scale is undefined by default
|
||||
expect(tlstate.getShape('text1').style.scale).toBeUndefined()
|
||||
expect(state.getShape('text1').style.scale).toBeUndefined()
|
||||
|
||||
// Transform the shape in order to change its point and scale
|
||||
|
||||
tlstate
|
||||
state
|
||||
.select('text1')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([-100, -100], false, false)
|
||||
.completeSession()
|
||||
|
||||
const scale = tlstate.getShape('text1').style.scale
|
||||
const bounds = TLDR.getBounds(tlstate.getShape('text1'))
|
||||
const scale = state.getShape('text1').style.scale
|
||||
const bounds = TLDR.getBounds(state.getShape('text1'))
|
||||
const center = Utils.getBoundsCenter(bounds)
|
||||
|
||||
expect(scale).not.toBe(1)
|
||||
|
@ -39,25 +39,25 @@ describe('Reset bounds command', () => {
|
|||
|
||||
// Reset the bounds
|
||||
|
||||
tlstate.resetBounds(['text1'])
|
||||
state.resetBounds(['text1'])
|
||||
|
||||
// The scale should be back to 1
|
||||
expect(tlstate.getShape('text1').style.scale).toBe(1)
|
||||
expect(state.getShape('text1').style.scale).toBe(1)
|
||||
// The centers should be the same
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(tlstate.getShape('text1')))).toStrictEqual(center)
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
// The scale should be what it was before
|
||||
expect(tlstate.getShape('text1').style.scale).not.toBe(1)
|
||||
expect(state.getShape('text1').style.scale).not.toBe(1)
|
||||
// The centers should be the same
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(tlstate.getShape('text1')))).toStrictEqual(center)
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
// The scale should be back to 1
|
||||
expect(tlstate.getShape('text1').style.scale).toBe(1)
|
||||
expect(state.getShape('text1').style.scale).toBe(1)
|
||||
// The centers should be the same
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(tlstate.getShape('text1')))).toStrictEqual(center)
|
||||
expect(Utils.getBoundsCenter(TLDR.getBounds(state.getShape('text1')))).toStrictEqual(center)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function resetBounds(data: Data, ids: string[], pageId: string): TLDrawCommand {
|
||||
export function resetBounds(data: TLDrawSnapshot, ids: string[], pageId: string): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const { before, after } = TLDR.mutateShapes(
|
||||
|
|
|
@ -2,38 +2,38 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Rotate command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.rotate()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.rotate()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect1')
|
||||
state.select('rect1')
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||
expect(state.getShape('rect1').rotation).toBe(undefined)
|
||||
|
||||
tlstate.rotate()
|
||||
state.rotate()
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI * (6 / 4))
|
||||
expect(state.getShape('rect1').rotation).toBe(Math.PI * (6 / 4))
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||
expect(state.getShape('rect1').rotation).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI * (6 / 4))
|
||||
expect(state.getShape('rect1').rotation).toBe(Math.PI * (6 / 4))
|
||||
})
|
||||
|
||||
it.todo('Rotates several shapes at once.')
|
||||
|
@ -43,17 +43,17 @@ describe('Rotate command', () => {
|
|||
|
||||
describe('when running the command', () => {
|
||||
it('restores selection on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.rotate()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.selectNone().redo()
|
||||
state.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { Utils } from '@tldraw/core'
|
||||
import type { TLDrawCommand, Data, TLDrawShape } from '~types'
|
||||
import type { TLDrawCommand, TLDrawSnapshot, TLDrawShape } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
const PI2 = Math.PI * 2
|
||||
|
||||
export function rotateShapes(data: Data, ids: string[], delta = -PI2 / 4): TLDrawCommand | void {
|
||||
export function rotateShapes(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
delta = -PI2 / 4
|
||||
): TLDrawCommand | void {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
// The shapes for the before patch
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TLDR } from '~state/TLDR'
|
||||
import type { ArrowShape, Data, GroupShape, PagePartial } from '~types'
|
||||
import type { ArrowShape, TLDrawSnapshot, GroupShape, PagePartial } from '~types'
|
||||
|
||||
export function removeShapesFromPage(data: Data, ids: string[], pageId: string) {
|
||||
export function removeShapesFromPage(data: TLDrawSnapshot, ids: string[], pageId: string) {
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
|
|
|
@ -4,88 +4,88 @@ import { mockDocument, TLDrawStateUtils } from '~test'
|
|||
import Vec from '@tldraw/vec'
|
||||
|
||||
describe('Stretch command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.select('rect2')
|
||||
const initialState = tlstate.state
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
const currentState = tlstate.state
|
||||
state.select('rect2')
|
||||
const initialState = state.state
|
||||
state.stretch(StretchType.Horizontal)
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
state.select('rect1', 'rect2')
|
||||
state.stretch(StretchType.Horizontal)
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').size).toStrictEqual([100, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').point).toStrictEqual([100, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').size).toStrictEqual([100, 100])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').size).toStrictEqual([100, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').point).toStrictEqual([100, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').size).toStrictEqual([100, 100])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
})
|
||||
|
||||
it('stretches horizontally', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
state.select('rect1', 'rect2')
|
||||
state.stretch(StretchType.Horizontal)
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').size).toStrictEqual([200, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').point).toStrictEqual([0, 100])
|
||||
expect(state.getShape<RectangleShape>('rect2').size).toStrictEqual([200, 100])
|
||||
})
|
||||
|
||||
it('stretches vertically', () => {
|
||||
tlstate.select('rect1', 'rect2')
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
state.select('rect1', 'rect2')
|
||||
state.stretch(StretchType.Vertical)
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').size).toStrictEqual([100, 200])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').point).toStrictEqual([100, 0])
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').size).toStrictEqual([100, 200])
|
||||
expect(state.getShape<RectangleShape>('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<RectangleShape>('rect1').size).toStrictEqual([100, 200])
|
||||
expect(state.getShape<RectangleShape>('rect2').point).toStrictEqual([100, 0])
|
||||
expect(state.getShape<RectangleShape>('rect2').size).toStrictEqual([100, 200])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when running the command', () => {
|
||||
it('restores selection on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.stretch(StretchType.Horizontal)
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
expect(state.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
|
||||
tlstate.selectNone().redo()
|
||||
state.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
expect(state.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when stretching groups', () => {
|
||||
it('stretches children', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||
{ id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||
|
@ -95,7 +95,7 @@ describe('when stretching groups', () => {
|
|||
.selectAll()
|
||||
.stretch(StretchType.Vertical)
|
||||
|
||||
new TLDrawStateUtils(tlstate).expectShapesToHaveProps({
|
||||
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] },
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLBoundsCorner, Utils } from '@tldraw/core'
|
||||
import { StretchType, TLDrawShapeType } from '~types'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function stretchShapes(data: Data, ids: string[], type: StretchType): TLDrawCommand {
|
||||
export function stretchShapes(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
type: StretchType
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
|
|
|
@ -5,69 +5,69 @@ import { SizeStyle, TLDrawShapeType } from '~types'
|
|||
|
||||
describe('Style command', () => {
|
||||
it('does, undoes and redoes command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
tlstate.loadDocument(mockDocument)
|
||||
tlstate.select('rect1')
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
const state = new TLDrawState()
|
||||
state.loadDocument(mockDocument)
|
||||
state.select('rect1')
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
|
||||
tlstate.style({ size: SizeStyle.Small })
|
||||
state.style({ size: SizeStyle.Small })
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
})
|
||||
|
||||
describe('When styling groups', () => {
|
||||
it('applies style to all group children', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
tlstate
|
||||
const state = new TLDrawState()
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.style({ size: SizeStyle.Small })
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(tlstate.getShape('rect2').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Small)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
expect(tlstate.getShape('rect2').style.size).toEqual(SizeStyle.Medium)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Medium)
|
||||
expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Medium)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(tlstate.getShape('rect2').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect1').style.size).toEqual(SizeStyle.Small)
|
||||
expect(state.getShape('rect2').style.size).toEqual(SizeStyle.Small)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When styling text', () => {
|
||||
it('recenters the shape if the size changed', () => {
|
||||
const tlstate = new TLDrawState().createShapes({
|
||||
const state = new TLDrawState().createShapes({
|
||||
id: 'text1',
|
||||
type: TLDrawShapeType.Text,
|
||||
text: 'Hello world',
|
||||
})
|
||||
|
||||
const centerA = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
const centerA = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1'))
|
||||
|
||||
tlstate.select('text1').style({ size: SizeStyle.Large })
|
||||
state.select('text1').style({ size: SizeStyle.Large })
|
||||
|
||||
const centerB = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
const centerB = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1'))
|
||||
|
||||
tlstate.style({ size: SizeStyle.Small })
|
||||
state.style({ size: SizeStyle.Small })
|
||||
|
||||
const centerC = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
const centerC = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1'))
|
||||
|
||||
tlstate.style({ size: SizeStyle.Medium })
|
||||
state.style({ size: SizeStyle.Medium })
|
||||
|
||||
const centerD = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
const centerD = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(state.getShape('text1'))
|
||||
|
||||
expect(centerA).toEqual(centerB)
|
||||
expect(centerA).toEqual(centerC)
|
||||
|
@ -78,17 +78,17 @@ describe('Style command', () => {
|
|||
|
||||
describe('when running the command', () => {
|
||||
it('restores selection on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.style({ size: SizeStyle.Small })
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.selectNone().redo()
|
||||
state.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import { ShapeStyles, TLDrawCommand, Data, TLDrawShape, TLDrawShapeType, TextShape } from '~types'
|
||||
import {
|
||||
ShapeStyles,
|
||||
TLDrawCommand,
|
||||
TLDrawSnapshot,
|
||||
TLDrawShape,
|
||||
TLDrawShapeType,
|
||||
TextShape,
|
||||
} from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { Patch } from 'rko'
|
||||
import Vec from '@tldraw/vec'
|
||||
|
||||
export function styleShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
changes: Partial<ShapeStyles>
|
||||
): TLDrawCommand {
|
||||
|
|
|
@ -4,17 +4,17 @@ import { mockDocument } from '~test'
|
|||
import { ArrowShape, Decoration, TLDrawShape, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Toggle decoration command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.toggleDecoration('start')
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.toggleDecoration('start')
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
|
@ -22,34 +22,34 @@ describe('Toggle decoration command', () => {
|
|||
|
||||
describe('when handle id is invalid', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.toggleDecoration('invalid')
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.toggleDecoration('invalid')
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate
|
||||
state
|
||||
.createShapes({
|
||||
id: 'arrow1',
|
||||
type: TLDrawShapeType.Arrow,
|
||||
})
|
||||
.select('arrow1')
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').decorations?.end).toBe(Decoration.Arrow)
|
||||
expect(state.getShape<ArrowShape>('arrow1').decorations?.end).toBe(Decoration.Arrow)
|
||||
|
||||
tlstate.toggleDecoration('end')
|
||||
state.toggleDecoration('end')
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').decorations?.end).toBe(undefined)
|
||||
expect(state.getShape<ArrowShape>('arrow1').decorations?.end).toBe(undefined)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').decorations?.end).toBe(Decoration.Arrow)
|
||||
expect(state.getShape<ArrowShape>('arrow1').decorations?.end).toBe(Decoration.Arrow)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').decorations?.end).toBe(undefined)
|
||||
expect(state.getShape<ArrowShape>('arrow1').decorations?.end).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Decoration } from '~types'
|
||||
import type { ArrowShape, TLDrawCommand, Data } from '~types'
|
||||
import type { ArrowShape, TLDrawCommand, TLDrawSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { Patch } from 'rko'
|
||||
|
||||
export function toggleShapesDecoration(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
decorationId: 'start' | 'end'
|
||||
): TLDrawCommand {
|
||||
|
|
|
@ -3,77 +3,77 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Toggle command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.toggleHidden()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.toggleHidden()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.selectAll()
|
||||
state.selectAll()
|
||||
|
||||
expect(tlstate.getShape('rect2').isLocked).toBe(undefined)
|
||||
expect(state.getShape('rect2').isLocked).toBe(undefined)
|
||||
|
||||
tlstate.toggleLocked()
|
||||
state.toggleLocked()
|
||||
|
||||
expect(tlstate.getShape('rect2').isLocked).toBe(true)
|
||||
expect(state.getShape('rect2').isLocked).toBe(true)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect2').isLocked).toBe(undefined)
|
||||
expect(state.getShape('rect2').isLocked).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect2').isLocked).toBe(true)
|
||||
expect(state.getShape('rect2').isLocked).toBe(true)
|
||||
})
|
||||
|
||||
it('toggles on before off when mixed values', () => {
|
||||
tlstate.select('rect2')
|
||||
state.select('rect2')
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(undefined)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
|
||||
expect(state.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(undefined)
|
||||
|
||||
tlstate.toggleAspectRatioLocked()
|
||||
state.toggleAspectRatioLocked()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
|
||||
expect(tlstate.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(true)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
|
||||
expect(state.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(true)
|
||||
|
||||
tlstate.selectAll()
|
||||
tlstate.toggleAspectRatioLocked()
|
||||
state.selectAll()
|
||||
state.toggleAspectRatioLocked()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
|
||||
|
||||
tlstate.toggleAspectRatioLocked()
|
||||
state.toggleAspectRatioLocked()
|
||||
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
|
||||
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
|
||||
expect(state.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when running the command', () => {
|
||||
it('restores selection on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.toggleHidden()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.selectNone().redo()
|
||||
state.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
expect(state.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import type { TLDrawShape, Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawShape, TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function toggleShapeProp(data: Data, ids: string[], prop: keyof TLDrawShape): TLDrawCommand {
|
||||
export function toggleShapeProp(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
prop: keyof TLDrawShape
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
|
|
|
@ -4,46 +4,46 @@ import { mockDocument, TLDrawStateUtils } from '~test'
|
|||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Translate command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.nudge([1, 2])
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.nudge([1, 2])
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.selectAll()
|
||||
tlstate.nudge([1, 2])
|
||||
state.selectAll()
|
||||
state.nudge([1, 2])
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([101, 102])
|
||||
expect(state.getShape('rect2').point).toEqual([101, 102])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([100, 100])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 100])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect2').point).toEqual([101, 102])
|
||||
expect(state.getShape('rect2').point).toEqual([101, 102])
|
||||
})
|
||||
|
||||
it('major nudges', () => {
|
||||
tlstate.selectAll()
|
||||
tlstate.nudge([1, 2], true)
|
||||
expect(tlstate.getShape('rect2').point).toEqual([110, 120])
|
||||
state.selectAll()
|
||||
state.nudge([1, 2], true)
|
||||
expect(state.getShape('rect2').point).toEqual([110, 120])
|
||||
})
|
||||
|
||||
describe('when nudging shapes with bindings', () => {
|
||||
it('deleted bindings if nudging shape is bound to other shapes', () => {
|
||||
tlstate
|
||||
state
|
||||
.resetDocument()
|
||||
.createShapes(
|
||||
{
|
||||
|
@ -63,26 +63,26 @@ describe('Translate command', () => {
|
|||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
const bindingId = state.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
|
||||
tlstate.select('arrow1').nudge([10, 10])
|
||||
state.select('arrow1').nudge([10, 10])
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeUndefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.getBinding(bindingId)).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeUndefined()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeDefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.getBinding(bindingId)).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeUndefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeUndefined()
|
||||
expect(state.getBinding(bindingId)).toBeUndefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not delete bindings if both bound and bound-to shapes are nudged', () => {
|
||||
tlstate
|
||||
state
|
||||
.resetDocument()
|
||||
.createShapes(
|
||||
{
|
||||
|
@ -102,34 +102,34 @@ describe('Translate command', () => {
|
|||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
const bindingId = state.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
|
||||
tlstate.select('arrow1', 'target1').nudge([10, 10])
|
||||
state.select('arrow1', 'target1').nudge([10, 10])
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeDefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.getBinding(bindingId)).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeDefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.getBinding(bindingId)).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getBinding(bindingId)).toBeDefined()
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
expect(state.getBinding(bindingId)).toBeDefined()
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When nudging groups', () => {
|
||||
it('nudges children instead', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.nudge([1, 1])
|
||||
|
||||
new TLDrawStateUtils(tlstate).expectShapesToBeAtPoints({
|
||||
new TLDrawStateUtils(state).expectShapesToBeAtPoints({
|
||||
rect1: [1, 1],
|
||||
rect2: [101, 101],
|
||||
})
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import { Data, TLDrawCommand, PagePartial, Session } from '~types'
|
||||
import { TLDrawSnapshot, TLDrawCommand, PagePartial, Session } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function translateShapes(data: Data, ids: string[], delta: number[]): TLDrawCommand {
|
||||
export function translateShapes(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
delta: number[]
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
// Clear session cache
|
||||
|
|
|
@ -4,48 +4,44 @@ import { mockDocument } from '~test'
|
|||
import { GroupShape, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Ungroup command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.ungroup()
|
||||
state.loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA').select('groupA').ungroup()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('groupA')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1').parentId).toBe('page1')
|
||||
expect(tlstate.getShape('rect2').parentId).toBe('page1')
|
||||
expect(state.getShape<GroupShape>('groupA')).toBeUndefined()
|
||||
expect(state.getShape('rect1').parentId).toBe('page1')
|
||||
expect(state.getShape('rect2').parentId).toBe('page1')
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('groupA')).toBeDefined()
|
||||
expect(tlstate.getShape<GroupShape>('groupA').children).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.getShape('rect1').parentId).toBe('groupA')
|
||||
expect(tlstate.getShape('rect2').parentId).toBe('groupA')
|
||||
expect(state.getShape<GroupShape>('groupA')).toBeDefined()
|
||||
expect(state.getShape<GroupShape>('groupA').children).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.getShape('rect1').parentId).toBe('groupA')
|
||||
expect(state.getShape('rect2').parentId).toBe('groupA')
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape<GroupShape>('groupA')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1').parentId).toBe('page1')
|
||||
expect(tlstate.getShape('rect2').parentId).toBe('page1')
|
||||
expect(state.getShape<GroupShape>('groupA')).toBeUndefined()
|
||||
expect(state.getShape('rect1').parentId).toBe('page1')
|
||||
expect(state.getShape('rect2').parentId).toBe('page1')
|
||||
})
|
||||
|
||||
describe('When ungrouping', () => {
|
||||
it('Ungroups shapes on any page', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.createPage('page2')
|
||||
.ungroup(['groupA'], 'page1')
|
||||
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
tlstate.undo()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeDefined()
|
||||
expect(state.getShape('groupA', 'page1')).toBeUndefined()
|
||||
state.undo()
|
||||
expect(state.getShape('groupA', 'page1')).toBeDefined()
|
||||
})
|
||||
|
||||
it('Ungroups multiple selected groups', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect4',
|
||||
|
@ -56,20 +52,20 @@ describe('Ungroup command', () => {
|
|||
.selectAll()
|
||||
.ungroup()
|
||||
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupB', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('groupA', 'page1')).toBeUndefined()
|
||||
expect(state.getShape('groupB', 'page1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('Does not ungroup if a group shape is not selected', () => {
|
||||
tlstate.loadDocument(mockDocument).select('rect1')
|
||||
const before = tlstate.state
|
||||
tlstate.group()
|
||||
state.loadDocument(mockDocument).select('rect1')
|
||||
const before = state.state
|
||||
state.group()
|
||||
// State should not have changed
|
||||
expect(tlstate.state).toStrictEqual(before)
|
||||
expect(state.state).toStrictEqual(before)
|
||||
})
|
||||
|
||||
it('Correctly selects children after ungrouping', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
|
@ -92,11 +88,11 @@ describe('Ungroup command', () => {
|
|||
.ungroup()
|
||||
|
||||
// State should not have changed
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect3', 'rect1', 'rect2'])
|
||||
expect(state.selectedIds).toStrictEqual(['rect3', 'rect1', 'rect2'])
|
||||
})
|
||||
|
||||
it('Reparents shapes to the page at the correct childIndex', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
|
@ -116,18 +112,18 @@ describe('Ungroup command', () => {
|
|||
)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const { childIndex } = tlstate.getShape<GroupShape>('groupA')
|
||||
const { childIndex } = state.getShape<GroupShape>('groupA')
|
||||
|
||||
expect(childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(3)
|
||||
expect(state.getShape('rect1').childIndex).toBe(1)
|
||||
expect(state.getShape('rect2').childIndex).toBe(2)
|
||||
expect(state.getShape('rect3').childIndex).toBe(3)
|
||||
|
||||
tlstate.ungroup()
|
||||
state.ungroup()
|
||||
|
||||
expect(tlstate.getShape('rect1').childIndex).toBe(1)
|
||||
expect(tlstate.getShape('rect2').childIndex).toBe(2)
|
||||
expect(tlstate.getShape('rect3').childIndex).toBe(3)
|
||||
expect(state.getShape('rect1').childIndex).toBe(1)
|
||||
expect(state.getShape('rect2').childIndex).toBe(2)
|
||||
expect(state.getShape('rect3').childIndex).toBe(3)
|
||||
})
|
||||
it.todo('Deletes any bindings to the group')
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { GroupShape, TLDrawBinding, TLDrawShape } from '~types'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { Patch } from 'rko'
|
||||
|
||||
export function ungroupShapes(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
selectedIds: string[],
|
||||
groupShapes: GroupShape[],
|
||||
pageId: string
|
||||
|
|
|
@ -2,33 +2,33 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
|
||||
describe('Update command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
beforeEach(() => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('when no shape is selected', () => {
|
||||
it('does nothing', () => {
|
||||
const initialState = tlstate.state
|
||||
tlstate.updateShapes()
|
||||
const currentState = tlstate.state
|
||||
const initialState = state.state
|
||||
state.updateShapes()
|
||||
const currentState = state.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate.updateShapes({ id: 'rect1', point: [100, 100] })
|
||||
state.updateShapes({ id: 'rect1', point: [100, 100] })
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([100, 100])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([100, 100])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([100, 100])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([100, 100])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { Data, TLDrawCommand, PagePartial, TLDrawShape } from '~types'
|
||||
import type { TLDrawSnapshot, TLDrawCommand, PagePartial, TLDrawShape } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export function update(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
updates: ({ id: string } & Partial<TLDrawShape>)[],
|
||||
pageId: string
|
||||
): TLDrawCommand {
|
||||
|
|
|
@ -9,7 +9,7 @@ describe('When migrating bindings', () => {
|
|||
})
|
||||
|
||||
it('migrates a document with an older version', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(oldDoc2 as unknown as TLDrawDocument)
|
||||
expect(tlstate.getShape('d7ab0a49-3cb3-43ae-3d83-f5cf2f4a510a').style.color).toBe('black')
|
||||
const state = new TLDrawState().loadDocument(oldDoc2 as unknown as TLDrawDocument)
|
||||
expect(state.getShape('d7ab0a49-3cb3-43ae-3d83-f5cf2f4a510a').style.color).toBe('black')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,129 +13,129 @@ describe('Arrow session', () => {
|
|||
).document
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const binding = tlstate.bindings[0]
|
||||
const binding = state.bindings[0]
|
||||
|
||||
expect(binding).toBeTruthy()
|
||||
expect(binding.fromId).toBe('arrow1')
|
||||
expect(binding.toId).toBe('target1')
|
||||
expect(binding.handleId).toBe('start')
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.bindings[0]).toBe(undefined)
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
expect(state.bindings[0]).toBe(undefined)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.bindings[0]).toBeTruthy()
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
expect(state.bindings[0]).toBeTruthy()
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.bindings[0]).toBe(undefined)
|
||||
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
expect(state.bindings[0]).toBe(undefined)
|
||||
expect(state.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
|
||||
})
|
||||
|
||||
describe('arrow binding', () => {
|
||||
it('points to the center', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
expect(tlstate.bindings[0].point).toStrictEqual([0.5, 0.5])
|
||||
expect(state.bindings[0].point).toStrictEqual([0.5, 0.5])
|
||||
})
|
||||
|
||||
it('Snaps to the center', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
expect(tlstate.bindings[0].point).toStrictEqual([0.5, 0.5])
|
||||
expect(state.bindings[0].point).toStrictEqual([0.5, 0.5])
|
||||
})
|
||||
|
||||
it('Binds at the bottom left', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([124, -24])
|
||||
expect(tlstate.bindings[0].point).toStrictEqual([1, 0])
|
||||
expect(state.bindings[0].point).toStrictEqual([1, 0])
|
||||
})
|
||||
|
||||
it('Cancels the bind when off of the expanded bounds', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([133, 133])
|
||||
|
||||
expect(tlstate.bindings[0]).toBe(undefined)
|
||||
expect(state.bindings[0]).toBe(undefined)
|
||||
})
|
||||
|
||||
it('binds on the inside of a shape while alt is held', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9])
|
||||
|
||||
expect(tlstate.bindings[0].point).toStrictEqual([0.71, 0.11])
|
||||
expect(state.bindings[0].point).toStrictEqual([0.71, 0.11])
|
||||
|
||||
tlstate.updateSession([91, 9], false, true, false)
|
||||
state.updateSession([91, 9], false, true, false)
|
||||
})
|
||||
|
||||
it('snaps to the inside center when the point is close to the center', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9], false, true, false)
|
||||
|
||||
expect(tlstate.bindings[0].point).toStrictEqual([0.78, 0.22])
|
||||
expect(state.bindings[0].point).toStrictEqual([0.78, 0.22])
|
||||
})
|
||||
|
||||
it('ignores binding when meta is held', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 45], false, false, true)
|
||||
|
||||
expect(tlstate.bindings.length).toBe(0)
|
||||
expect(state.bindings.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when dragging a bound shape', () => {
|
||||
it('updates the arrow', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate.loadDocument(restoreDoc)
|
||||
state.loadDocument(restoreDoc)
|
||||
// Select the arrow and begin a session on the handle's start handle
|
||||
tlstate.select('arrow1').startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
state.select('arrow1').startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
// Move to [50,50]
|
||||
tlstate.updateSession([50, 50])
|
||||
state.updateSession([50, 50])
|
||||
// Both handles will keep the same screen positions, but their points will have changed.
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
|
||||
expect(state.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
|
||||
})
|
||||
|
||||
it.todo('updates the arrow when bound on both sides')
|
||||
|
@ -146,7 +146,7 @@ describe('Arrow session', () => {
|
|||
|
||||
describe('When creating with an arrow session', () => {
|
||||
it('Deletes the shape on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes({ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] })
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start', true)
|
||||
|
@ -154,11 +154,11 @@ describe('When creating with an arrow session', () => {
|
|||
.completeSession()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.getShape('arrow1')).toBe(undefined)
|
||||
expect(state.getShape('arrow1')).toBe(undefined)
|
||||
})
|
||||
|
||||
it("Doesn't corrupt a shape after undoing", () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] },
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] },
|
||||
|
@ -169,20 +169,20 @@ describe('When creating with an arrow session', () => {
|
|||
.updateSession([55, 45])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(2)
|
||||
expect(state.bindings.length).toBe(2)
|
||||
|
||||
tlstate
|
||||
state
|
||||
.undo()
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Translate, [250, 250])
|
||||
.updateSession([275, 275])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(0)
|
||||
expect(state.bindings.length).toBe(0)
|
||||
})
|
||||
|
||||
it('Creates a start binding if possible', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] },
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] },
|
||||
|
@ -193,18 +193,18 @@ describe('When creating with an arrow session', () => {
|
|||
.updateSession([450, 250])
|
||||
.completeSession()
|
||||
|
||||
const arrow = tlstate.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape
|
||||
const arrow = state.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape
|
||||
|
||||
expect(arrow).toBeTruthy()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(2)
|
||||
expect(state.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 tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect1', point: [200, 200], size: [200, 200] },
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'rect2', point: [400, 200], size: [200, 200] },
|
||||
|
@ -219,11 +219,11 @@ describe('When creating with an arrow session', () => {
|
|||
.updateSession([0, 0])
|
||||
.completeSession()
|
||||
|
||||
const arrow = tlstate.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape
|
||||
const arrow = state.shapes.find((shape) => shape.type === TLDrawShapeType.Arrow) as ArrowShape
|
||||
|
||||
expect(arrow).toBeTruthy()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
expect(state.bindings.length).toBe(1)
|
||||
|
||||
expect(arrow.handles.start.point).toStrictEqual([0, 0])
|
||||
expect(arrow.handles.start.bindingId).toBe(undefined)
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ArrowShape,
|
||||
TLDrawShape,
|
||||
TLDrawBinding,
|
||||
Data,
|
||||
TLDrawSnapshot,
|
||||
Session,
|
||||
TLDrawStatus,
|
||||
SessionType,
|
||||
|
@ -33,7 +33,7 @@ export class ArrowSession extends Session {
|
|||
isCreate: boolean
|
||||
|
||||
constructor(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
viewport: TLBounds,
|
||||
point: number[],
|
||||
handleId: 'start' | 'end',
|
||||
|
@ -85,7 +85,13 @@ export class ArrowSession extends Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
update = (
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
) => {
|
||||
const { initialShape } = this
|
||||
|
||||
const page = TLDR.getPage(data, data.appState.currentPageId)
|
||||
|
@ -312,7 +318,7 @@ export class ArrowSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const { initialShape, initialBinding, newStartBindingId, draggedBindingId } = this
|
||||
|
||||
const afterBindings: Record<string, TLDrawBinding | undefined> = {}
|
||||
|
@ -349,7 +355,7 @@ export class ArrowSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
|
||||
|
||||
const page = TLDR.getPage(data, data.appState.currentPageId)
|
||||
|
|
|
@ -4,39 +4,39 @@ import { SessionType, TLDrawStatus } from '~types'
|
|||
|
||||
describe('Brush session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(tlstate.selectedIds.length).toBe(1)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.selectedIds.length).toBe(1)
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([110, 110])
|
||||
.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(3)
|
||||
expect(state.selectedIds.length).toBe(3)
|
||||
})
|
||||
|
||||
it('does not de-select original shapes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Brush, [300, 300])
|
||||
.updateSession([301, 301])
|
||||
.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(1)
|
||||
expect(state.selectedIds.length).toBe(1)
|
||||
})
|
||||
|
||||
// it('does not select hidden shapes', () => {
|
||||
// const tlstate = new TLDrawState()
|
||||
// const state = new TLDrawState()
|
||||
// .loadDocument(mockDocument)
|
||||
// .selectNone()
|
||||
// .toggleHidden(['rect1'])
|
||||
|
@ -47,7 +47,7 @@ describe('Brush session', () => {
|
|||
// })
|
||||
|
||||
it('when command is held, require the entire shape to be selected', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.loadDocument(mockDocument)
|
||||
|
@ -56,6 +56,6 @@ describe('Brush session', () => {
|
|||
.updateSession([10, 10], false, false, true)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.selectedIds.length).toBe(0)
|
||||
expect(state.selectedIds.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Utils, TLBounds } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Data, Session, SessionType, TLDrawPatch, TLDrawStatus } from '~types'
|
||||
import { TLDrawSnapshot, Session, SessionType, TLDrawPatch, TLDrawStatus } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export class BrushSession extends Session {
|
||||
|
@ -9,7 +9,7 @@ export class BrushSession extends Session {
|
|||
origin: number[]
|
||||
snapshot: BrushSnapshot
|
||||
|
||||
constructor(data: Data, viewport: TLBounds, point: number[]) {
|
||||
constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[]) {
|
||||
super(viewport)
|
||||
this.origin = Vec.round(point)
|
||||
this.snapshot = getBrushSnapshot(data)
|
||||
|
@ -18,7 +18,7 @@ export class BrushSession extends Session {
|
|||
start = () => void null
|
||||
|
||||
update = (
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
_shiftKey = false,
|
||||
_altKey = false,
|
||||
|
@ -79,7 +79,7 @@ export class BrushSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const { currentPageId } = data.appState
|
||||
return {
|
||||
document: {
|
||||
|
@ -93,7 +93,7 @@ export class BrushSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const { currentPageId } = data.appState
|
||||
const pageState = TLDR.getPageState(data, currentPageId)
|
||||
|
||||
|
@ -115,7 +115,7 @@ export class BrushSession extends Session {
|
|||
* 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: Data) {
|
||||
export function getBrushSnapshot(data: TLDrawSnapshot) {
|
||||
const { currentPageId } = data.appState
|
||||
const selectedIds = [...TLDR.getSelectedIds(data, currentPageId)]
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ import {
|
|||
} from '~types'
|
||||
|
||||
describe('Draw session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
expect(tlstate.getShape('draw1')).toBe(undefined)
|
||||
expect(state.getShape('draw1')).toBe(undefined)
|
||||
|
||||
tlstate
|
||||
state
|
||||
.createShapes({
|
||||
id: 'draw1',
|
||||
parentId: 'page1',
|
||||
|
@ -37,18 +37,18 @@ describe('Draw session', () => {
|
|||
.updateSession([10, 10, 0.5])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
})
|
||||
|
||||
it('does, undoes and redoes', () => {
|
||||
expect(tlstate.getShape('draw1')).toBeTruthy()
|
||||
expect(state.getShape('draw1')).toBeTruthy()
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('draw1')).toBe(undefined)
|
||||
expect(state.getShape('draw1')).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('draw1')).toBeTruthy()
|
||||
expect(state.getShape('draw1')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Utils, TLBounds } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Data, Session, SessionType, TLDrawStatus } from '~types'
|
||||
import { TLDrawSnapshot, Session, SessionType, TLDrawStatus } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export class DrawSession extends Session {
|
||||
|
@ -16,7 +16,7 @@ export class DrawSession extends Session {
|
|||
isLocked?: boolean
|
||||
lockedDirection?: 'horizontal' | 'vertical'
|
||||
|
||||
constructor(data: Data, viewport: TLBounds, point: number[], id: string) {
|
||||
constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[], id: string) {
|
||||
super(viewport)
|
||||
this.origin = point
|
||||
this.previous = point
|
||||
|
@ -32,7 +32,13 @@ export class DrawSession extends Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
update = (
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
) => {
|
||||
const { shapeId } = this
|
||||
|
||||
// Even if we're not locked yet, we base the future locking direction
|
||||
|
@ -145,7 +151,7 @@ export class DrawSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const { shapeId } = this
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
|
@ -167,7 +173,7 @@ export class DrawSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const { shapeId } = this
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
|
|
|
@ -3,40 +3,40 @@ import { mockDocument } from '~test'
|
|||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Grid session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Translate, [5, 5])
|
||||
.updateSession([10, 10])
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
|
||||
tlstate.completeSession()
|
||||
state.completeSession()
|
||||
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startSession(SessionType.Translate, [5, 5])
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
TLDrawShape,
|
||||
TLDrawBinding,
|
||||
Session,
|
||||
Data,
|
||||
TLDrawSnapshot,
|
||||
TLDrawCommand,
|
||||
TLDrawStatus,
|
||||
ArrowShape,
|
||||
|
@ -29,7 +29,13 @@ export class GridSession extends Session {
|
|||
rows = 1
|
||||
isCopying = false
|
||||
|
||||
constructor(data: Data, viewport: TLBounds, id: string, pageId: string, point: number[]) {
|
||||
constructor(
|
||||
data: TLDrawSnapshot,
|
||||
viewport: TLBounds,
|
||||
id: string,
|
||||
pageId: string,
|
||||
point: number[]
|
||||
) {
|
||||
super(viewport)
|
||||
this.origin = point
|
||||
this.shape = TLDR.getShape(data, id, pageId)
|
||||
|
@ -61,7 +67,13 @@ export class GridSession extends Session {
|
|||
return clone
|
||||
}
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
update = (
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
) => {
|
||||
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
||||
const nextPageState: Patch<TLPageState> = {}
|
||||
|
@ -156,7 +168,7 @@ export class GridSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
|
||||
|
||||
// Delete clones
|
||||
|
@ -190,7 +202,7 @@ export class GridSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
const beforeShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
|
|
@ -3,10 +3,10 @@ import { mockDocument } from '~test'
|
|||
import { SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Handle session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'arrow1',
|
||||
|
@ -17,13 +17,13 @@ describe('Handle session', () => {
|
|||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
|
||||
tlstate.undo().redo()
|
||||
state.undo().redo()
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
type: TLDrawShapeType.Arrow,
|
||||
|
@ -34,6 +34,6 @@ describe('Handle session', () => {
|
|||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Vec } from '@tldraw/vec'
|
|||
import type { TLBounds } from '@tldraw/core'
|
||||
import { SessionType, ShapesWithProp, TLDrawStatus } from '~types'
|
||||
import { Session } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export class HandleSession extends Session {
|
||||
|
@ -17,7 +17,7 @@ export class HandleSession extends Session {
|
|||
handleId: string
|
||||
|
||||
constructor(
|
||||
data: Data,
|
||||
data: TLDrawSnapshot,
|
||||
viewport: TLBounds,
|
||||
point: number[],
|
||||
handleId: string,
|
||||
|
@ -35,7 +35,13 @@ export class HandleSession extends Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
update = (
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
) => {
|
||||
const { initialShape } = this
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
|
@ -77,7 +83,7 @@ export class HandleSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const { initialShape } = this
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
|
@ -94,7 +100,7 @@ export class HandleSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const { initialShape } = this
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
|
|
|
@ -5,80 +5,78 @@ import { mockDocument } from '~test'
|
|||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Rotate session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||
expect(state.getShape('rect1').rotation).toBe(undefined)
|
||||
|
||||
tlstate.select('rect1').startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50])
|
||||
state.select('rect1').startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI / 2)
|
||||
expect(state.getShape('rect1').rotation).toBe(Math.PI / 2)
|
||||
|
||||
tlstate.updateSession([50, 100])
|
||||
state.updateSession([50, 100])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI)
|
||||
expect(state.getShape('rect1').rotation).toBe(Math.PI)
|
||||
|
||||
tlstate.updateSession([0, 50])
|
||||
state.updateSession([0, 50])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
|
||||
tlstate.updateSession([50, 0])
|
||||
state.updateSession([50, 0])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(0)
|
||||
expect(state.getShape('rect1').rotation).toBe(0)
|
||||
|
||||
tlstate.updateSession([0, 50])
|
||||
state.updateSession([0, 50])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
|
||||
tlstate.completeSession()
|
||||
state.completeSession()
|
||||
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||
expect(state.getShape('rect1').rotation).toBe(undefined)
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
expect(state.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Rotate, [50, 0])
|
||||
.updateSession([100, 50])
|
||||
.cancel()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.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 tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||
0
|
||||
)
|
||||
expect(Math.round((state.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 tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
|
@ -86,56 +84,52 @@ describe('Rotate session', () => {
|
|||
.completeSession()
|
||||
|
||||
// Rect 1 clamp rotated, starting from a little rotation
|
||||
tlstate
|
||||
state
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([100, 200], true)
|
||||
.completeSession()
|
||||
|
||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||
0
|
||||
)
|
||||
expect(Math.round((state.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0)
|
||||
|
||||
// Try again, too.
|
||||
tlstate
|
||||
state
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([-100, 5000], true)
|
||||
.completeSession()
|
||||
|
||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||
0
|
||||
)
|
||||
expect(Math.round((state.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when rotating multiple shapes', () => {
|
||||
it('keeps the center', () => {
|
||||
tlstate.loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
state.loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
|
||||
const centerBefore = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
|
||||
const centerAfterA = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
tlstate.startSession(SessionType.Rotate, [100, 0]).updateSession([50, 0]).completeSession()
|
||||
state.startSession(SessionType.Rotate, [100, 0]).updateSession([50, 0]).completeSession()
|
||||
|
||||
const centerAfterB = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation)
|
||||
expect(state.getShape('rect1').rotation)
|
||||
expect(centerBefore).toStrictEqual(centerAfterA)
|
||||
expect(centerAfterA).toStrictEqual(centerAfterB)
|
||||
})
|
||||
|
@ -147,32 +141,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 tlstate = new TLDrawState().loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
const state = new TLDrawState().loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
|
||||
const centerBefore = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
|
||||
const centerAfterA = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation)
|
||||
expect(state.getShape('rect1').rotation)
|
||||
expect(centerBefore).toStrictEqual(centerAfterA)
|
||||
|
||||
tlstate.selectAll().nudge([10, 10])
|
||||
state.selectAll().nudge([10, 10])
|
||||
|
||||
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
state.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50]).completeSession()
|
||||
|
||||
const centerAfterB = Vec.round(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(tlstate.selectedIds.map((id) => tlstate.getShapeBounds(id)))
|
||||
Utils.getCommonBounds(state.selectedIds.map((id) => state.getShapeBounds(id)))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Utils, TLBounds } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
export class RotateSession extends Session {
|
||||
|
@ -13,7 +13,7 @@ export class RotateSession extends Session {
|
|||
initialAngle: number
|
||||
changes: Record<string, Partial<TLDrawShape>> = {}
|
||||
|
||||
constructor(data: Data, viewport: TLBounds, point: number[]) {
|
||||
constructor(data: TLDrawSnapshot, viewport: TLBounds, point: number[]) {
|
||||
super(viewport)
|
||||
|
||||
this.origin = point
|
||||
|
@ -23,7 +23,13 @@ export class RotateSession extends Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
update = (
|
||||
data: TLDrawSnapshot,
|
||||
point: number[],
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
metaKey = false
|
||||
) => {
|
||||
const { commonBoundsCenter, initialShapes } = this.snapshot
|
||||
|
||||
const pageId = data.appState.currentPageId
|
||||
|
@ -71,7 +77,7 @@ export class RotateSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel = (data: TLDrawSnapshot) => {
|
||||
const { initialShapes } = this.snapshot
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
|
@ -92,7 +98,7 @@ export class RotateSession extends Session {
|
|||
}
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
complete = (data: TLDrawSnapshot) => {
|
||||
const { initialShapes } = this.snapshot
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
|
@ -128,7 +134,7 @@ export class RotateSession extends Session {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function getRotateSnapshot(data: Data) {
|
||||
export function getRotateSnapshot(data: TLDrawSnapshot) {
|
||||
const currentPageId = data.appState.currentPageId
|
||||
const pageState = TLDR.getPageState(data, currentPageId)
|
||||
const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId)
|
||||
|
|
|
@ -4,19 +4,19 @@ import { TLBoundsCorner, Utils } from '@tldraw/core'
|
|||
import { TLDR } from '~state/TLDR'
|
||||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
||||
function getShapeBounds(state: TLDrawState, ...ids: string[]) {
|
||||
return Utils.getCommonBounds(
|
||||
(ids.length ? ids : tlstate.selectedIds).map((id) => TLDR.getBounds(tlstate.getShape(id)))
|
||||
(ids.length ? ids : state.selectedIds).map((id) => TLDR.getBounds(state.getShape(id)))
|
||||
)
|
||||
}
|
||||
|
||||
describe('Transform session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 100,
|
||||
|
@ -25,15 +25,15 @@ describe('Transform session', () => {
|
|||
height: 100,
|
||||
})
|
||||
|
||||
tlstate
|
||||
state
|
||||
.select('rect1', 'rect2')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status).toBe(TLDrawStatus.Idle)
|
||||
expect(state.appState.status).toBe(TLDrawStatus.Idle)
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 105,
|
||||
|
@ -42,9 +42,9 @@ describe('Transform session', () => {
|
|||
height: 95,
|
||||
})
|
||||
|
||||
tlstate.undo()
|
||||
state.undo()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 100,
|
||||
|
@ -53,30 +53,30 @@ describe('Transform session', () => {
|
|||
height: 100,
|
||||
})
|
||||
|
||||
tlstate.redo()
|
||||
state.redo()
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(state.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
})
|
||||
|
||||
describe('when transforming from the top-left corner', () => {
|
||||
it('transforms a single shape', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate)).toMatchObject({
|
||||
expect(getShapeBounds(state)).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 100,
|
||||
|
@ -87,14 +87,14 @@ describe('Transform session', () => {
|
|||
})
|
||||
|
||||
it('transforms a single shape while holding shift', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 100,
|
||||
|
@ -105,14 +105,14 @@ describe('Transform session', () => {
|
|||
})
|
||||
|
||||
it('transforms multiple shapes', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 105,
|
||||
|
@ -121,7 +121,7 @@ describe('Transform session', () => {
|
|||
height: 95,
|
||||
})
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect2')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect2')).toMatchObject({
|
||||
minX: 105,
|
||||
minY: 105,
|
||||
maxX: 200,
|
||||
|
@ -132,14 +132,14 @@ describe('Transform session', () => {
|
|||
})
|
||||
|
||||
it('transforms multiple shapes while holding shift', () => {
|
||||
tlstate
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 105,
|
||||
|
@ -148,7 +148,7 @@ describe('Transform session', () => {
|
|||
height: 95,
|
||||
})
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect2')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect2')).toMatchObject({
|
||||
minX: 105,
|
||||
minY: 105,
|
||||
maxX: 200,
|
||||
|
@ -189,8 +189,8 @@ describe('Transform session', () => {
|
|||
|
||||
describe('when transforming a group', () => {
|
||||
it('transforms the groups children', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
tlstate
|
||||
const state = new TLDrawState()
|
||||
state
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
|
@ -198,7 +198,7 @@ describe('Transform session', () => {
|
|||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect1')).toMatchObject({
|
||||
minX: 10,
|
||||
minY: 10,
|
||||
maxX: 105,
|
||||
|
@ -207,7 +207,7 @@ describe('Transform session', () => {
|
|||
height: 95,
|
||||
})
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect2')).toMatchObject({
|
||||
expect(getShapeBounds(state, 'rect2')).toMatchObject({
|
||||
minX: 105,
|
||||
minY: 105,
|
||||
maxX: 200,
|
||||
|
@ -221,7 +221,7 @@ describe('Transform session', () => {
|
|||
|
||||
describe('When creating with a transform session', () => {
|
||||
it('Deletes the shape on undo', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft, true)
|
||||
|
@ -229,7 +229,7 @@ describe('When creating with a transform session', () => {
|
|||
.completeSession()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
||||
expect(state.getShape('rect1')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue