[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:
Steve Ruiz 2021-11-08 14:21:37 +00:00 committed by GitHub
parent f037118928
commit be2c6d6d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
131 changed files with 3938 additions and 3674 deletions

143
README.md
View file

@ -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.

View file

@ -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
}
}

View file

@ -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,
})

View file

@ -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",

View file

@ -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) {

View file

@ -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) => {

View file

@ -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} />

View file

@ -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,

View file

@ -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}
/>

View file

@ -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"
]
},

View file

@ -1,4 +1,4 @@
## 0.1.6
## 0.1.4
- UI bug fixes.

View file

@ -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`

View file

@ -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": {

View file

@ -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,

View file

@ -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>

View file

@ -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)

View file

@ -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>
)
})

View file

@ -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>

View file

@ -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">

View file

@ -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()

View file

@ -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()

View file

@ -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]}

View file

@ -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>

View file

@ -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 (

View file

@ -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>

View file

@ -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 (

View file

@ -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(() => {

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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]
)

View file

@ -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 {

View file

@ -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]
)
}

View file

@ -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)

View file

@ -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,
}
}

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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))

View file

@ -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)
})
})

View file

@ -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: {

View file

@ -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)
})
})

View file

@ -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(

View file

@ -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')

View file

@ -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 {

View file

@ -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)
})
})

View file

@ -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(

View file

@ -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()
})
})

View file

@ -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 {

View file

@ -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,

View file

@ -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))

View file

@ -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)
})
})

View file

@ -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

View file

@ -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))))

View file

@ -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)

View file

@ -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])
})
})

View file

@ -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))

View file

@ -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)
})
/*

View file

@ -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

View file

@ -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'])
})
})

View file

@ -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,

View file

@ -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')
})
})

View file

@ -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',

View file

@ -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')
})
})
})

View file

@ -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

View file

@ -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)
})
})

View file

@ -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(

View file

@ -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'])
})
})

View file

@ -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

View file

@ -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: {},

View file

@ -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] },

View file

@ -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))

View file

@ -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'])
})
})

View file

@ -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 {

View file

@ -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)
})
})

View file

@ -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 {

View file

@ -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'])
})
})

View file

@ -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))

View file

@ -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],
})

View file

@ -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

View file

@ -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')
})

View file

@ -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

View file

@ -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])
})
})

View file

@ -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 {

View file

@ -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')
})
})

View file

@ -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)

View file

@ -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)

View file

@ -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)
})
})

View file

@ -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)]

View file

@ -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()
})
})

View file

@ -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

View file

@ -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])
})
})

View file

@ -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>> = {}

View file

@ -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])
})
})

View file

@ -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

View file

@ -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)))
)
)

View file

@ -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)

View file

@ -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