Implements multiple pages

This commit is contained in:
Steve Ruiz 2021-08-17 22:38:37 +01:00
parent ad3db2c0ac
commit 07dcfb8df5
53 changed files with 22276 additions and 887 deletions

18
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "tsc",
"type": "shell",
"command": "./node_modules/.bin/tsc",
"args": ["--noEmit"],
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "dedicated"
},
"problemMatcher": "$tsc-watch"
}
]
}

View file

@ -75,6 +75,39 @@ const defaultTheme: TLTheme = {
} }
const tlcss = css` const tlcss = css`
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive Mono';
font-style: normal;
font-weight: 420;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root { :root {
--tl-zoom: 1; --tl-zoom: 1;
--tl-scale: calc(1 / var(--tl-zoom)); --tl-scale: calc(1 / var(--tl-zoom));

View file

@ -102,5 +102,5 @@ export default function Editor(): JSX.Element {
return <div /> return <div />
} }
return <TLDraw document={initialDoc} onChange={handleChange} /> return <TLDraw document={value} onChange={handleChange} />
} }

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react' import * as React from 'react'
import { openDB, DBSchema } from 'idb' import { openDB, DBSchema, deleteDB } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw' import type { TLDrawDocument } from '@tldraw/tldraw'
const VERSION = 1 const VERSION = 1
@ -57,6 +57,8 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state. // the state.
React.useEffect(() => { React.useEffect(() => {
async function handleLoad() { async function handleLoad() {
await deleteDB('db')
const db = await openDB<TLDatabase>('db', VERSION, { const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) { upgrade(db) {
db.createObjectStore('documents') db.createObjectStore('documents')

View file

@ -0,0 +1 @@
export * from './menu'

View file

@ -15,7 +15,7 @@ import type { Data, TLDrawPage } from '~types'
import { useTLDrawContext } from '~hooks' import { useTLDrawContext } from '~hooks'
const canDeleteSelector = (s: Data) => { const canDeleteSelector = (s: Data) => {
return Object.keys(s.document.pages).length <= 1 return Object.keys(s.document.pages).length > 1
} }
export function PageOptionsDialog({ page }: { page: TLDrawPage }): JSX.Element { export function PageOptionsDialog({ page }: { page: TLDrawPage }): JSX.Element {

View file

@ -73,7 +73,7 @@ export function PagePanel(): JSX.Element {
value={page.id} value={page.id}
variant="pageButton" variant="pageButton"
> >
<span>{page.name}</span> <span>{page.name || 'Page'}</span>
<DropdownMenu.ItemIndicator> <DropdownMenu.ItemIndicator>
<IconWrapper size="small"> <IconWrapper size="small">
<CheckIcon /> <CheckIcon />

View file

@ -9,6 +9,8 @@ import { tldrawShapeUtils } from '~shape'
import { ContextMenu } from '~components/context-menu' import { ContextMenu } from '~components/context-menu'
import { StylePanel } from '~components/style-panel' import { StylePanel } from '~components/style-panel'
import { ToolsPanel } from '~components/tools-panel' import { ToolsPanel } from '~components/tools-panel'
import { PagePanel } from '~components/page-panel'
import { Menu } from '~components/menu'
export interface TLDrawProps { export interface TLDrawProps {
document?: TLDrawDocument document?: TLDrawDocument
@ -126,7 +128,10 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
onTextKeyUp={tlstate.onTextKeyUp} onTextKeyUp={tlstate.onTextKeyUp}
/> />
</ContextMenu> </ContextMenu>
<MenuButtons /> <MenuButtons>
<Menu />
<PagePanel />
</MenuButtons>
<Spacer /> <Spacer />
<StylePanel /> <StylePanel />
<ToolsPanel /> <ToolsPanel />

View file

@ -4,7 +4,8 @@ import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function align(data: Data, ids: string[], type: AlignType): Command { export function align(data: Data, ids: string[], type: AlignType): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const boundsForShapes = initialShapes.map((shape) => { const boundsForShapes = initialShapes.map((shape) => {
return { return {
@ -38,17 +39,22 @@ export function align(data: Data, ids: string[], type: AlignType): Command {
}) })
) )
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => { const { before, after } = TLDR.mutateShapes(
if (!deltaMap[shape.id]) return shape data,
return { point: deltaMap[shape.id].next } ids,
}) (shape) => {
if (!deltaMap[shape.id]) return shape
return { point: deltaMap[shape.id].next }
},
currentPageId
)
return { return {
id: 'align_shapes', id: 'align_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: before, shapes: before,
}, },
}, },
@ -57,7 +63,7 @@ export function align(data: Data, ids: string[], type: AlignType): Command {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: after, shapes: after,
}, },
}, },

View file

@ -17,12 +17,16 @@ describe('Change page command', () => {
expect(tlstate.page.id).toBe(initialId) expect(tlstate.page.id).toBe(initialId)
tlstate.undo() tlstate.changePage(nextId)
expect(tlstate.page.id).toBe(nextId) expect(tlstate.page.id).toBe(nextId)
tlstate.redo() tlstate.undo()
expect(tlstate.page.id).toBe(initialId) expect(tlstate.page.id).toBe(initialId)
tlstate.redo()
expect(tlstate.page.id).toBe(nextId)
}) })
}) })

View file

@ -1,10 +1,18 @@
import type { TLDrawShape, Data, Command } from '~types' import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function changePage(data: Data): Command { export function changePage(data: Data, pageId: string): Command {
return { return {
id: 'create_page', id: 'change_page',
before: {}, before: {
after: {}, appState: {
currentPageId: data.appState.currentPageId,
},
},
after: {
appState: {
currentPageId: pageId,
},
},
} }
} }

View file

@ -8,19 +8,27 @@ describe('Create page command', () => {
tlstate.loadDocument(mockDocument) tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id const initialId = tlstate.page.id
const initialPageState = tlstate.pageState
tlstate.createPage() tlstate.createPage()
const nextId = tlstate.page.id const nextId = tlstate.page.id
const nextPageState = tlstate.pageState
expect(Object.keys(tlstate.document.pages).length).toBe(2)
expect(tlstate.page.id).toBe(nextId) expect(tlstate.page.id).toBe(nextId)
expect(tlstate.pageState).toEqual(nextPageState)
tlstate.undo() tlstate.undo()
expect(Object.keys(tlstate.document.pages).length).toBe(1)
expect(tlstate.page.id).toBe(initialId) expect(tlstate.page.id).toBe(initialId)
expect(tlstate.pageState).toEqual(initialPageState)
tlstate.redo() tlstate.redo()
expect(Object.keys(tlstate.document.pages).length).toBe(2)
expect(tlstate.page.id).toBe(nextId) expect(tlstate.page.id).toBe(nextId)
expect(tlstate.pageState).toEqual(nextPageState)
}) })
}) })

View file

@ -1,10 +1,46 @@
import type { TLDrawShape, Data, Command } from '~types' import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { Utils } from '@tldraw/core'
export function createPage(data: Data): Command { export function createPage(data: Data): Command {
const newId = Utils.uniqueId()
const { currentPageId } = data.appState
return { return {
id: 'create_page', id: 'create_page',
before: {}, before: {
after: {}, appState: {
currentPageId,
},
document: {
pages: {
[newId]: undefined,
},
pageStates: {
[newId]: undefined,
},
},
},
after: {
appState: {
currentPageId: newId,
},
document: {
pages: {
[newId]: { id: newId, shapes: {}, bindings: {} },
},
pageStates: {
[newId]: {
id: newId,
selectedIds: [],
camera: { point: [-window.innerWidth / 2, -window.innerHeight / 2], zoom: 1 },
currentParentId: newId,
editingId: undefined,
bindingId: undefined,
hoveredId: undefined,
pointedId: undefined,
},
},
},
},
} }
} }

View file

@ -3,6 +3,7 @@ import { TLDR } from '~state/tldr'
import type { TLDrawShape, Data, Command } from '~types' import type { TLDrawShape, Data, Command } from '~types'
export function create(data: Data, shapes: TLDrawShape[]): Command { export function create(data: Data, shapes: TLDrawShape[]): Command {
const { currentPageId } = data.appState
const beforeShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {} const beforeShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
const afterShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {} const afterShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
@ -16,13 +17,13 @@ export function create(data: Data, shapes: TLDrawShape[]): Command {
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: beforeShapes, shapes: beforeShapes,
}, },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data)], selectedIds: [...TLDR.getSelectedIds(data, currentPageId)],
}, },
}, },
}, },
@ -30,12 +31,12 @@ export function create(data: Data, shapes: TLDrawShape[]): Command {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: afterShapes, shapes: afterShapes,
}, },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: {
selectedIds: shapes.map((shape) => shape.id), selectedIds: shapes.map((shape) => shape.id),
}, },
}, },

View file

@ -7,22 +7,22 @@ describe('Delete page', () => {
it('does, undoes and redoes command', () => { it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument) tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id const initialId = tlstate.currentPageId
tlstate.createPage() tlstate.createPage()
const nextId = tlstate.page.id const nextId = tlstate.currentPageId
tlstate.deletePage() tlstate.deletePage()
expect(tlstate.page.id).toBe(nextId) expect(tlstate.currentPageId).toBe(initialId)
tlstate.undo() tlstate.undo()
expect(tlstate.page.id).toBe(initialId) expect(tlstate.currentPageId).toBe(nextId)
tlstate.redo() tlstate.redo()
expect(tlstate.page.id).toBe(nextId) expect(tlstate.currentPageId).toBe(initialId)
}) })
}) })

View file

@ -1,10 +1,53 @@
import type { TLDrawShape, Data, Command } from '~types' import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function deletePage(data: Data, pageId: string): Command {
const { currentPageId } = data.appState
const pagesArr = Object.values(data.document.pages).sort(
(a, b) => (a.childIndex || 0) - (b.childIndex || 0)
)
const currentIndex = pagesArr.findIndex((page) => page.id === pageId)
let nextCurrentPageId: string
if (pageId === currentPageId) {
if (currentIndex === pagesArr.length - 1) {
nextCurrentPageId = pagesArr[pagesArr.length - 2].id
} else {
nextCurrentPageId = pagesArr[currentIndex + 1].id
}
} else {
nextCurrentPageId = currentPageId
}
export function deletePage(data: Data, id: string): Command {
return { return {
id: 'delete_page', id: 'delete_page',
before: {}, before: {
after: {}, appState: {
currentPageId: pageId,
},
document: {
pages: {
[pageId]: { ...data.document.pages[pageId] },
},
pageStates: {
[pageId]: { ...data.document.pageStates[pageId] },
},
},
},
after: {
appState: {
currentPageId: nextCurrentPageId,
},
document: {
pages: {
[pageId]: undefined,
},
pageStates: {
[pageId]: undefined,
},
},
},
} }
} }

View file

@ -6,6 +6,8 @@ import type { Data, Command, PagePartial } from '~types'
// - [ ] Update parents and possibly delete parents // - [ ] Update parents and possibly delete parents
export function deleteShapes(data: Data, ids: string[]): Command { export function deleteShapes(data: Data, ids: string[]): Command {
const { currentPageId } = data.appState
const before: PagePartial = { const before: PagePartial = {
shapes: {}, shapes: {},
bindings: {}, bindings: {},
@ -18,11 +20,11 @@ export function deleteShapes(data: Data, ids: string[]): Command {
// These are the shapes we're definitely going to delete // These are the shapes we're definitely going to delete
ids.forEach((id) => { ids.forEach((id) => {
before.shapes[id] = TLDR.getShape(data, id) before.shapes[id] = TLDR.getShape(data, id, currentPageId)
after.shapes[id] = undefined after.shapes[id] = undefined
}) })
const page = TLDR.getPage(data) const page = TLDR.getPage(data, currentPageId)
// We also need to delete bindings that reference the deleted shapes // We also need to delete bindings that reference the deleted shapes
Object.values(page.bindings).forEach((binding) => { Object.values(page.bindings).forEach((binding) => {
@ -34,7 +36,7 @@ export function deleteShapes(data: Data, ids: string[]): Command {
after.bindings[binding.id] = undefined after.bindings[binding.id] = undefined
// Let's also look at the bound shape... // Let's also look at the bound shape...
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id, currentPageId)
// If the bound shape has a handle that references the deleted binding, delete that reference // If the bound shape has a handle that references the deleted binding, delete that reference
if (shape.handles) { if (shape.handles) {
@ -60,20 +62,20 @@ export function deleteShapes(data: Data, ids: string[]): Command {
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: before, [currentPageId]: before,
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { selectedIds: TLDR.getSelectedIds(data) }, [currentPageId]: { selectedIds: TLDR.getSelectedIds(data, currentPageId) },
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: after, [currentPageId]: after,
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { selectedIds: [] }, [currentPageId]: { selectedIds: [] },
}, },
}, },
}, },

View file

@ -3,27 +3,33 @@ import { DistributeType, TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function distribute(data: Data, ids: string[], type: DistributeType): Command { export function distribute(data: Data, ids: string[], type: DistributeType): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const deltaMap = Object.fromEntries(getDistributions(initialShapes, type).map((d) => [d.id, d])) const deltaMap = Object.fromEntries(getDistributions(initialShapes, type).map((d) => [d.id, d]))
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => { const { before, after } = TLDR.mutateShapes(
if (!deltaMap[shape.id]) return shape data,
return { point: deltaMap[shape.id].next } ids,
}) (shape) => {
if (!deltaMap[shape.id]) return shape
return { point: deltaMap[shape.id].next }
},
currentPageId
)
return { return {
id: 'distribute_shapes', id: 'distribute_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -9,7 +9,7 @@ describe('Duplicate page', () => {
const initialId = tlstate.page.id const initialId = tlstate.page.id
tlstate.duplicatePage() tlstate.duplicatePage(tlstate.currentPageId)
const nextId = tlstate.page.id const nextId = tlstate.page.id

View file

@ -3,11 +3,12 @@ import { TLDR } from '~state/tldr'
import type { Data, Command } from '~types' import type { Data, Command } from '~types'
export function duplicate(data: Data, ids: string[]): Command { export function duplicate(data: Data, ids: string[]): Command {
const delta = Vec.div([16, 16], TLDR.getCamera(data).zoom) const { currentPageId } = data.appState
const delta = Vec.div([16, 16], TLDR.getCamera(data, currentPageId).zoom)
const after = Object.fromEntries( const after = Object.fromEntries(
TLDR.getSelectedIds(data) TLDR.getSelectedIds(data, currentPageId)
.map((id) => TLDR.getShape(data, id)) .map((id) => TLDR.getShape(data, id, currentPageId))
.map((shape) => { .map((shape) => {
const id = Utils.uniqueId() const id = Utils.uniqueId()
return [ return [
@ -28,20 +29,20 @@ export function duplicate(data: Data, ids: string[]): Command {
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { selectedIds: ids }, [currentPageId]: { selectedIds: ids },
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { selectedIds: Object.keys(after) }, [currentPageId]: { selectedIds: Object.keys(after) },
}, },
}, },
}, },

View file

@ -4,52 +4,58 @@ import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function flip(data: Data, ids: string[], type: FlipType): Command { export function flip(data: Data, ids: string[], type: FlipType): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape))
const commonBounds = Utils.getCommonBounds(boundsForShapes) const commonBounds = Utils.getCommonBounds(boundsForShapes)
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => { const { before, after } = TLDR.mutateShapes(
const shapeBounds = TLDR.getBounds(shape) data,
ids,
(shape) => {
const shapeBounds = TLDR.getBounds(shape)
switch (type) { switch (type) {
case FlipType.Horizontal: { case FlipType.Horizontal: {
const newShapeBounds = Utils.getRelativeTransformedBoundingBox( const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
commonBounds, commonBounds,
commonBounds, commonBounds,
shapeBounds, shapeBounds,
true, true,
false false
) )
return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, { return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, {
type: TLBoundsCorner.TopLeft, type: TLBoundsCorner.TopLeft,
scaleX: -1, scaleX: -1,
scaleY: 1, scaleY: 1,
initialShape: shape, initialShape: shape,
transformOrigin: [0.5, 0.5], transformOrigin: [0.5, 0.5],
}) })
}
case FlipType.Vertical: {
const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
commonBounds,
commonBounds,
shapeBounds,
false,
true
)
return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, {
type: TLBoundsCorner.TopLeft,
scaleX: 1,
scaleY: -1,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
}
} }
case FlipType.Vertical: { },
const newShapeBounds = Utils.getRelativeTransformedBoundingBox( currentPageId
commonBounds, )
commonBounds,
shapeBounds,
false,
true
)
return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, {
type: TLBoundsCorner.TopLeft,
scaleX: 1,
scaleY: -1,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
}
}
})
return { return {
id: 'flip_shapes', id: 'flip_shapes',

View file

@ -11,3 +11,8 @@ export * from './toggle'
export * from './translate' export * from './translate'
export * from './flip' export * from './flip'
export * from './toggle-decoration' export * from './toggle-decoration'
export * from './create-page'
export * from './delete-page'
export * from './rename-page'
export * from './duplicate-page'
export * from './change-page'

View file

@ -33,7 +33,7 @@ delete doc.pages.page1.shapes['rect2']
delete doc.pages.page1.shapes['rect3'] delete doc.pages.page1.shapes['rect3']
function getSortedShapeIds(data: Data) { function getSortedShapeIds(data: Data) {
return TLDR.getShapes(data) return TLDR.getShapes(data, data.appState.currentPageId)
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id) .map((shape) => shape.id)
.join('') .join('')

View file

@ -2,8 +2,10 @@ import { MoveType, Data, TLDrawShape, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function move(data: Data, ids: string[], type: MoveType): Command { export function move(data: Data, ids: string[], type: MoveType): Command {
const { currentPageId } = data.appState
// Get the unique parent ids for the selected elements // Get the unique parent ids for the selected elements
const parentIds = new Set(ids.map((id) => TLDR.getShape(data, id).parentId)) const parentIds = new Set(ids.map((id) => TLDR.getShape(data, id, currentPageId).parentId))
let result: { let result: {
before: Record<string, Partial<TLDrawShape>> before: Record<string, Partial<TLDrawShape>>
@ -14,7 +16,7 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
let startChildIndex: number let startChildIndex: number
let step: number let step: number
const page = TLDR.getPage(data) const page = TLDR.getPage(data, currentPageId)
// Collect shapes with common parents into a table under their parent id // Collect shapes with common parents into a table under their parent id
Array.from(parentIds.values()).forEach((parentId) => { Array.from(parentIds.values()).forEach((parentId) => {
@ -22,11 +24,11 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
if (parentId === page.id) { if (parentId === page.id) {
sortedChildren = Object.values(page.shapes).sort((a, b) => a.childIndex - b.childIndex) sortedChildren = Object.values(page.shapes).sort((a, b) => a.childIndex - b.childIndex)
} else { } else {
const parent = TLDR.getShape(data, parentId) const parent = TLDR.getShape(data, parentId, currentPageId)
if (!parent.children) throw Error('No children in parent!') if (!parent.children) throw Error('No children in parent!')
sortedChildren = parent.children sortedChildren = parent.children
.map((childId) => TLDR.getShape(data, childId)) .map((childId) => TLDR.getShape(data, childId, currentPageId))
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
} }
@ -65,7 +67,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id).reverse(), sortedIndicesToMove.map((i) => sortedChildren[i].id).reverse(),
(_shape, i) => ({ (_shape, i) => ({
childIndex: startChildIndex - (i + 1) * step, childIndex: startChildIndex - (i + 1) * step,
}) }),
currentPageId
) )
break break
@ -95,7 +98,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id), sortedIndicesToMove.map((i) => sortedChildren[i].id),
(_shape, i) => ({ (_shape, i) => ({
childIndex: startChildIndex + (i + 1), childIndex: startChildIndex + (i + 1),
}) }),
currentPageId
) )
break break
@ -140,7 +144,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id), sortedIndicesToMove.map((i) => sortedChildren[i].id),
(shape) => ({ (shape) => ({
childIndex: indexMap[shape.id], childIndex: indexMap[shape.id],
}) }),
currentPageId
) )
} }
@ -188,7 +193,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id), sortedIndicesToMove.map((i) => sortedChildren[i].id),
(shape) => ({ (shape) => ({
childIndex: indexMap[shape.id], childIndex: indexMap[shape.id],
}) }),
currentPageId
) )
} }

View file

@ -1,10 +1,22 @@
import type { TLDrawShape, Data, Command } from '~types' import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function editPage(data: Data, id: string): Command { export function renamePage(data: Data, pageId: string, name: string): Command {
const page = data.document.pages[pageId]
return { return {
id: 'edit_page', id: 'rename_page',
before: {}, before: {
after: {}, document: {
pages: {
[pageId]: { name: page.name },
},
},
},
after: {
document: {
pages: {
[pageId]: { name: name },
},
},
},
} }
} }

View file

@ -5,7 +5,8 @@ import { TLDR } from '~state/tldr'
const PI2 = Math.PI * 2 const PI2 = Math.PI * 2
export function rotate(data: Data, ids: string[], delta = -PI2 / 4): Command { export function rotate(data: Data, ids: string[], delta = -PI2 / 4): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const boundsForShapes = initialShapes.map((shape) => { const boundsForShapes = initialShapes.map((shape) => {
const utils = TLDR.getShapeUtils(shape) const utils = TLDR.getShapeUtils(shape)
@ -31,35 +32,36 @@ export function rotate(data: Data, ids: string[], delta = -PI2 / 4): Command {
}) })
) )
const pageState = TLDR.getPageState(data) const pageState = TLDR.getPageState(data, currentPageId)
const prevBoundsRotation = pageState.boundsRotation const prevBoundsRotation = pageState.boundsRotation
const nextBoundsRotation = (PI2 + ((pageState.boundsRotation || 0) + delta)) % PI2 const nextBoundsRotation = (PI2 + ((pageState.boundsRotation || 0) + delta)) % PI2
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => rotations[shape.id]) const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => rotations[shape.id],
currentPageId
)
return { return {
id: 'toggle_shapes', id: 'toggle_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: { boundsRotation: prevBoundsRotation },
boundsRotation: prevBoundsRotation,
},
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: { boundsRotation: nextBoundsRotation },
boundsRotation: nextBoundsRotation,
},
}, },
}, },
}, },

View file

@ -4,64 +4,71 @@ import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function stretch(data: Data, ids: string[], type: StretchType): Command { export function stretch(data: Data, ids: string[], type: StretchType): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape))
const commonBounds = Utils.getCommonBounds(boundsForShapes) const commonBounds = Utils.getCommonBounds(boundsForShapes)
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => { const { before, after } = TLDR.mutateShapes(
const bounds = TLDR.getBounds(shape) data,
ids,
(shape) => {
const bounds = TLDR.getBounds(shape)
switch (type) { switch (type) {
case StretchType.Horizontal: { case StretchType.Horizontal: {
const newBounds = { const newBounds = {
...bounds, ...bounds,
minX: commonBounds.minX, minX: commonBounds.minX,
maxX: commonBounds.maxX, maxX: commonBounds.maxX,
width: commonBounds.width, width: commonBounds.width,
}
return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
type: TLBoundsCorner.TopLeft,
scaleX: newBounds.width / bounds.width,
scaleY: 1,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
} }
case StretchType.Vertical: {
const newBounds = {
...bounds,
minY: commonBounds.minY,
maxY: commonBounds.maxY,
height: commonBounds.height,
}
return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
type: TLBoundsCorner.TopLeft, type: TLBoundsCorner.TopLeft,
scaleX: newBounds.width / bounds.width, scaleX: 1,
scaleY: 1, scaleY: newBounds.height / bounds.height,
initialShape: shape, initialShape: shape,
transformOrigin: [0.5, 0.5], transformOrigin: [0.5, 0.5],
}) })
}
case StretchType.Vertical: {
const newBounds = {
...bounds,
minY: commonBounds.minY,
maxY: commonBounds.maxY,
height: commonBounds.height,
} }
return TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
type: TLBoundsCorner.TopLeft,
scaleX: 1,
scaleY: newBounds.height / bounds.height,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
} }
} },
}) currentPageId
)
return { return {
id: 'stretch_shapes', id: 'stretch_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -2,16 +2,23 @@ import type { ShapeStyles, Command, Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): Command { export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): Command {
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => { const { currentPageId } = data.appState
return { style: { ...shape.style, ...changes } }
}) const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
return { style: { ...shape.style, ...changes } }
},
currentPageId
)
return { return {
id: 'style_shapes', id: 'style_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
}, },
appState: { appState: {
@ -21,7 +28,7 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
}, },
appState: { appState: {

View file

@ -3,34 +3,40 @@ import type { ArrowShape, Command, Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function toggleDecoration(data: Data, ids: string[], handleId: 'start' | 'end'): Command { export function toggleDecoration(data: Data, ids: string[], handleId: 'start' | 'end'): Command {
const { before, after } = TLDR.mutateShapes<ArrowShape>(data, ids, (shape) => { const { currentPageId } = data.appState
const decorations = shape.decorations const { before, after } = TLDR.mutateShapes<ArrowShape>(
? { data,
...shape.decorations, ids,
[handleId]: shape.decorations[handleId] ? undefined : Decoration.Arrow, (shape) => {
} const decorations = shape.decorations
: { ? {
[handleId]: Decoration.Arrow, ...shape.decorations,
} [handleId]: shape.decorations[handleId] ? undefined : Decoration.Arrow,
}
: {
[handleId]: Decoration.Arrow,
}
return { return {
decorations, decorations,
} }
}) },
currentPageId
)
return { return {
id: 'toggle_decorations', id: 'toggle_decorations',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: before }, [currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { shapes: after }, [currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -2,19 +2,25 @@ import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Command { export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Command {
const initialShapes = ids.map((id) => TLDR.getShape(data, id)) const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const isAllToggled = initialShapes.every((shape) => shape[prop]) const isAllToggled = initialShapes.every((shape) => shape[prop])
const { before, after } = TLDR.mutateShapes(data, TLDR.getSelectedIds(data), () => ({ const { before, after } = TLDR.mutateShapes(
[prop]: !isAllToggled, data,
})) TLDR.getSelectedIds(data, currentPageId),
() => ({
[prop]: !isAllToggled,
}),
currentPageId
)
return { return {
id: 'toggle_shapes', id: 'toggle_shapes',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: before, shapes: before,
}, },
}, },
@ -23,7 +29,7 @@ export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Comm
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: after, shapes: after,
}, },
}, },

View file

@ -13,14 +13,19 @@ export function translate(data: Data, ids: string[], delta: number[]): Command {
bindings: {}, bindings: {},
} }
const change = TLDR.mutateShapes(data, ids, (shape) => ({ const change = TLDR.mutateShapes(
point: Vec.round(Vec.add(shape.point, delta)), data,
})) ids,
(shape) => ({
point: Vec.round(Vec.add(shape.point, delta)),
}),
data.appState.currentPageId
)
before.shapes = change.before before.shapes = change.before
after.shapes = change.after after.shapes = change.after
const bindingsToDelete = TLDR.getRelatedBindings(data, ids) const bindingsToDelete = TLDR.getRelatedBindings(data, ids, data.appState.currentPageId)
bindingsToDelete.forEach((binding) => { bindingsToDelete.forEach((binding) => {
before.bindings[binding.id] = binding before.bindings[binding.id] = binding
@ -28,7 +33,7 @@ export function translate(data: Data, ids: string[], delta: number[]): Command {
for (const id of [binding.toId, binding.fromId]) { for (const id of [binding.toId, binding.fromId]) {
// Let's also look at the bound shape... // Let's also look at the bound shape...
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id, data.appState.currentPageId)
// If the bound shape has a handle that references the deleted binding, delete that reference // If the bound shape has a handle that references the deleted binding, delete that reference
if (!shape.handles) continue if (!shape.handles) continue

View file

@ -1,7 +1,7 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import type { ArrowShape, TLDrawShape } from '~types' import { ArrowShape, TLDrawShape, TLDrawStatus } from '~types'
describe('Arrow session', () => { describe('Arrow session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -41,6 +41,7 @@ describe('Arrow session', () => {
expect(binding.fromId).toBe('arrow1') expect(binding.fromId).toBe('arrow1')
expect(binding.toId).toBe('target1') expect(binding.toId).toBe('target1')
expect(binding.handleId).toBe('start') expect(binding.handleId).toBe('start')
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id) expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(binding.id)
tlstate.undo() tlstate.undo()

View file

@ -31,7 +31,7 @@ export class ArrowSession implements Session {
const shapeId = pageState.selectedIds[0] const shapeId = pageState.selectedIds[0]
this.origin = point this.origin = point
this.handleId = handleId this.handleId = handleId
this.initialShape = TLDR.getShape<ArrowShape>(data, shapeId) this.initialShape = TLDR.getShape<ArrowShape>(data, shapeId, data.appState.currentPageId)
this.bindableShapeIds = TLDR.getBindableShapeIds(data) this.bindableShapeIds = TLDR.getBindableShapeIds(data)
const initialBindingId = this.initialShape.handles[this.handleId].bindingId const initialBindingId = this.initialShape.handles[this.handleId].bindingId
@ -47,12 +47,11 @@ export class ArrowSession implements Session {
start = (data: Data) => data start = (data: Data) => data
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => { update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
const page = TLDR.getPage(data) const page = TLDR.getPage(data, data.appState.currentPageId)
const pageState = TLDR.getPageState(data)
const { initialShape } = this const { initialShape } = this
const shape = TLDR.getShape<ArrowShape>(data, initialShape.id) const shape = TLDR.getShape<ArrowShape>(data, initialShape.id, data.appState.currentPageId)
const handles = shape.handles const handles = shape.handles
@ -102,7 +101,7 @@ export class ArrowSession implements Session {
if (id === initialShape.id) continue if (id === initialShape.id) continue
if (id === oppositeBinding?.toId) continue if (id === oppositeBinding?.toId) continue
const target = TLDR.getShape(data, id) const target = TLDR.getShape(data, id, data.appState.currentPageId)
const util = TLDR.getShapeUtils(target) const util = TLDR.getShapeUtils(target)
@ -231,12 +230,16 @@ export class ArrowSession implements Session {
complete(data: Data) { complete(data: Data) {
const { initialShape, initialBinding, handleId } = this const { initialShape, initialBinding, handleId } = this
const page = TLDR.getPage(data) const page = TLDR.getPage(data, data.appState.currentPageId)
const beforeBindings: Partial<Record<string, TLDrawBinding>> = {} const beforeBindings: Partial<Record<string, TLDrawBinding>> = {}
const afterBindings: Partial<Record<string, TLDrawBinding>> = {} const afterBindings: Partial<Record<string, TLDrawBinding>> = {}
const currentShape = TLDR.getShape<ArrowShape>(data, initialShape.id) const currentShape = TLDR.getShape<ArrowShape>(
data,
initialShape.id,
data.appState.currentPageId
)
const currentBindingId = currentShape.handles[handleId].bindingId const currentBindingId = currentShape.handles[handleId].bindingId
if (initialBinding) { if (initialBinding) {
@ -275,7 +278,8 @@ export class ArrowSession implements Session {
shapes: { shapes: {
[initialShape.id]: TLDR.onSessionComplete( [initialShape.id]: TLDR.onSessionComplete(
data, data,
TLDR.getShape(data, initialShape.id) TLDR.getShape(data, initialShape.id, data.appState.currentPageId),
data.appState.currentPageId
), ),
}, },
bindings: afterBindings, bindings: afterBindings,

View file

@ -1,5 +1,6 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLDrawStatus } from '~types'
describe('Brush session', () => { describe('Brush session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -10,6 +11,7 @@ describe('Brush session', () => {
tlstate.startBrushSession([-10, -10]) tlstate.startBrushSession([-10, -10])
tlstate.updateBrushSession([10, 10]) tlstate.updateBrushSession([10, 10])
tlstate.completeSession() tlstate.completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
expect(tlstate.selectedIds.length).toBe(1) expect(tlstate.selectedIds.length).toBe(1)
}) })

View file

@ -19,6 +19,7 @@ export class BrushSession implements Session {
update = (data: Data, point: number[], containMode = false): DeepPartial<Data> => { update = (data: Data, point: number[], containMode = false): DeepPartial<Data> => {
const { snapshot, origin } = this const { snapshot, origin } = this
const { currentPageId } = data.appState
// Create a bounding box between the origin and the new point // Create a bounding box between the origin and the new point
const brush = Utils.getBoundsFromPoints([origin, point]) const brush = Utils.getBoundsFromPoints([origin, point])
@ -29,8 +30,8 @@ export class BrushSession implements Session {
const hits = new Set<string>() const hits = new Set<string>()
const selectedIds = new Set(snapshot.selectedIds) const selectedIds = new Set(snapshot.selectedIds)
const page = TLDR.getPage(data) const page = TLDR.getPage(data, currentPageId)
const pageState = TLDR.getPageState(data) const pageState = TLDR.getPageState(data, currentPageId)
snapshot.shapesToTest.forEach(({ id, util, selectId }) => { snapshot.shapesToTest.forEach(({ id, util, selectId }) => {
if (selectedIds.has(id)) return if (selectedIds.has(id)) return
@ -65,7 +66,7 @@ export class BrushSession implements Session {
return { return {
document: { document: {
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: {
selectedIds: Array.from(selectedIds.values()), selectedIds: Array.from(selectedIds.values()),
}, },
}, },
@ -74,10 +75,11 @@ export class BrushSession implements Session {
} }
cancel(data: Data) { cancel(data: Data) {
const { currentPageId } = data.appState
return { return {
document: { document: {
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: {
selectedIds: this.snapshot.selectedIds, selectedIds: this.snapshot.selectedIds,
}, },
}, },
@ -86,11 +88,12 @@ export class BrushSession implements Session {
} }
complete(data: Data) { complete(data: Data) {
const pageState = TLDR.getPageState(data) const { currentPageId } = data.appState
const pageState = TLDR.getPageState(data, currentPageId)
return { return {
document: { document: {
pageStates: { pageStates: {
[data.appState.currentPageId]: { [currentPageId]: {
selectedIds: [...pageState.selectedIds], selectedIds: [...pageState.selectedIds],
}, },
}, },
@ -105,9 +108,10 @@ export class BrushSession implements Session {
* brush will intersect that shape. For tests, start broad -> fine. * brush will intersect that shape. For tests, start broad -> fine.
*/ */
export function getBrushSnapshot(data: Data) { export function getBrushSnapshot(data: Data) {
const selectedIds = [...TLDR.getSelectedIds(data)] const { currentPageId } = data.appState
const selectedIds = [...TLDR.getSelectedIds(data, currentPageId)]
const shapesToTest = TLDR.getShapes(data) const shapesToTest = TLDR.getShapes(data, currentPageId)
.filter( .filter(
(shape) => (shape) =>
!( !(
@ -121,7 +125,7 @@ export function getBrushSnapshot(data: Data) {
id: shape.id, id: shape.id,
util: getShapeUtils(shape), util: getShapeUtils(shape),
bounds: getShapeUtils(shape).getBounds(shape), bounds: getShapeUtils(shape).getBounds(shape),
selectId: TLDR.getTopParentId(data, shape.id), selectId: TLDR.getTopParentId(data, shape.id, currentPageId),
})) }))
return { return {

View file

@ -1,6 +1,6 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType } from '~types' import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType, TLDrawStatus } from '~types'
describe('Transform session', () => { describe('Transform session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -29,6 +29,8 @@ describe('Transform session', () => {
.startDrawSession('draw1', [0, 0]) .startDrawSession('draw1', [0, 0])
.updateDrawSession([10, 10], 0.5) .updateDrawSession([10, 10], 0.5)
.completeSession() .completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
}) })
it('does, undoes and redoes', () => { it('does, undoes and redoes', () => {

View file

@ -110,18 +110,19 @@ export class DrawSession implements Session {
cancel = (data: Data) => { cancel = (data: Data) => {
const { snapshot } = this const { snapshot } = this
const pageId = data.appState.currentPageId
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[snapshot.id]: undefined, [snapshot.id]: undefined,
}, },
}, },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [pageId]: {
selectedIds: [], selectedIds: [],
}, },
}, },
@ -131,19 +132,20 @@ export class DrawSession implements Session {
complete = (data: Data) => { complete = (data: Data) => {
const { snapshot } = this const { snapshot } = this
const pageId = data.appState.currentPageId
return { return {
id: 'create_draw', id: 'create_draw',
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[snapshot.id]: undefined, [snapshot.id]: undefined,
}, },
}, },
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [pageId]: {
selectedIds: [], selectedIds: [],
}, },
}, },
@ -152,9 +154,13 @@ export class DrawSession implements Session {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[snapshot.id]: TLDR.onSessionComplete(data, TLDR.getShape(data, snapshot.id)), [snapshot.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, snapshot.id, pageId),
pageId
),
}, },
}, },
}, },
@ -171,7 +177,7 @@ export class DrawSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getDrawSnapshot(data: Data, shapeId: string) { export function getDrawSnapshot(data: Data, shapeId: string) {
const page = { ...TLDR.getPage(data) } const page = { ...TLDR.getPage(data, data.appState.currentPageId) }
const { points, point } = Utils.deepClone(page.shapes[shapeId]) as DrawShape const { points, point } = Utils.deepClone(page.shapes[shapeId]) as DrawShape

View file

@ -1,7 +1,7 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import type { TLDrawShape } from '~types' import { TLDrawShape, TLDrawStatus } from '~types'
describe('Handle session', () => { describe('Handle session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -19,8 +19,10 @@ describe('Handle session', () => {
.startHandleSession([-10, -10], 'end') .startHandleSession([-10, -10], 'end')
.updateHandleSession([10, 10]) .updateHandleSession([10, 10])
.completeSession() .completeSession()
.undo()
.redo() expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
tlstate.undo().redo()
}) })
it('cancels session', () => { it('cancels session', () => {

View file

@ -15,10 +15,11 @@ export class HandleSession implements Session {
handleId: string handleId: string
constructor(data: Data, handleId: string, point: number[], commandId = 'move_handle') { constructor(data: Data, handleId: string, point: number[], commandId = 'move_handle') {
const shapeId = TLDR.getSelectedIds(data)[0] const { currentPageId } = data.appState
const shapeId = TLDR.getSelectedIds(data, currentPageId)[0]
this.origin = point this.origin = point
this.handleId = handleId this.handleId = handleId
this.initialShape = TLDR.getShape(data, shapeId) this.initialShape = TLDR.getShape(data, shapeId, currentPageId)
this.commandId = commandId this.commandId = commandId
} }
@ -26,8 +27,9 @@ export class HandleSession implements Session {
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => { update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
const { initialShape } = this const { initialShape } = this
const { currentPageId } = data.appState
const shape = TLDR.getShape<ShapesWithProp<'handles'>>(data, initialShape.id) const shape = TLDR.getShape<ShapesWithProp<'handles'>>(data, initialShape.id, currentPageId)
const handles = shape.handles const handles = shape.handles
@ -54,7 +56,7 @@ export class HandleSession implements Session {
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: { shapes: {
[shape.id]: change, [shape.id]: change,
}, },
@ -66,11 +68,12 @@ export class HandleSession implements Session {
cancel = (data: Data) => { cancel = (data: Data) => {
const { initialShape } = this const { initialShape } = this
const { currentPageId } = data.appState
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [currentPageId]: {
shapes: { shapes: {
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
@ -82,12 +85,14 @@ export class HandleSession implements Session {
complete(data: Data) { complete(data: Data) {
const { initialShape } = this const { initialShape } = this
const pageId = data.appState.currentPageId
return { return {
id: this.commandId, id: this.commandId,
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
@ -98,11 +103,12 @@ export class HandleSession implements Session {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[initialShape.id]: TLDR.onSessionComplete( [initialShape.id]: TLDR.onSessionComplete(
data, data,
TLDR.getShape(data, this.initialShape.id) TLDR.getShape(data, this.initialShape.id, pageId),
pageId
), ),
}, },
}, },

View file

@ -1,5 +1,6 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLDrawStatus } from '~types'
describe('Brush session', () => { describe('Brush session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -34,6 +35,8 @@ describe('Brush session', () => {
tlstate.completeSession() tlstate.completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
tlstate.undo() tlstate.undo()
expect(tlstate.getShape('rect1').rotation).toBe(undefined) expect(tlstate.getShape('rect1').rotation).toBe(undefined)

View file

@ -2,7 +2,6 @@ import { Utils, Vec } from '@tldraw/core'
import { Session, TLDrawShape, TLDrawStatus } from '~types' import { Session, TLDrawShape, TLDrawStatus } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
const PI2 = Math.PI * 2 const PI2 = Math.PI * 2
@ -23,8 +22,9 @@ export class RotateSession implements Session {
update = (data: Data, point: number[], isLocked = false) => { update = (data: Data, point: number[], isLocked = false) => {
const { commonBoundsCenter, initialShapes } = this.snapshot const { commonBoundsCenter, initialShapes } = this.snapshot
const page = TLDR.getPage(data) const pageId = data.appState.currentPageId
const pageState = TLDR.getPageState(data) const page = TLDR.getPage(data, pageId)
const pageState = TLDR.getPageState(data, pageId)
const shapes: Record<string, TLDrawShape> = {} const shapes: Record<string, TLDrawShape> = {}
@ -54,16 +54,21 @@ export class RotateSession implements Session {
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset) const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset)
shapes[id] = TLDR.mutate(data, shape, { shapes[id] = TLDR.mutate(
point: nextPoint, data,
rotation: (PI2 + nextRotation) % PI2, shape,
}) {
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
},
pageId
)
}) })
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes, shapes,
}, },
}, },
@ -73,6 +78,7 @@ export class RotateSession implements Session {
cancel = (data: Data) => { cancel = (data: Data) => {
const { initialShapes } = this.snapshot const { initialShapes } = this.snapshot
const pageId = data.appState.currentPageId
const shapes: Record<string, TLDrawShape> = {} const shapes: Record<string, TLDrawShape> = {}
@ -83,7 +89,7 @@ export class RotateSession implements Session {
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes, shapes,
}, },
}, },
@ -93,6 +99,7 @@ export class RotateSession implements Session {
complete(data: Data) { complete(data: Data) {
const { hasUnlockedShapes, initialShapes } = this.snapshot const { hasUnlockedShapes, initialShapes } = this.snapshot
const pageId = data.appState.currentPageId
if (!hasUnlockedShapes) return data if (!hasUnlockedShapes) return data
@ -101,7 +108,7 @@ export class RotateSession implements Session {
initialShapes.forEach(({ id, shape: { point, rotation } }) => { initialShapes.forEach(({ id, shape: { point, rotation } }) => {
beforeShapes[id] = { point, rotation } beforeShapes[id] = { point, rotation }
const afterShape = TLDR.getShape(data, id) const afterShape = TLDR.getShape(data, id, pageId)
afterShapes[id] = { point: afterShape.point, rotation: afterShape.rotation } afterShapes[id] = { point: afterShape.point, rotation: afterShape.rotation }
}) })
@ -110,7 +117,7 @@ export class RotateSession implements Session {
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: beforeShapes, shapes: beforeShapes,
}, },
}, },
@ -119,7 +126,7 @@ export class RotateSession implements Session {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: afterShapes, shapes: afterShapes,
}, },
}, },
@ -131,8 +138,9 @@ export class RotateSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getRotateSnapshot(data: Data) { export function getRotateSnapshot(data: Data) {
const pageState = TLDR.getPageState(data) const currentPageId = data.appState.currentPageId
const initialShapes = TLDR.getSelectedBranchSnapshot(data) const pageState = TLDR.getPageState(data, currentPageId)
const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId)
if (initialShapes.length === 0) { if (initialShapes.length === 0) {
throw Error('No selected shapes!') throw Error('No selected shapes!')

View file

@ -9,14 +9,16 @@ export class TextSession implements Session {
initialShape: TextShape initialShape: TextShape
constructor(data: Data, id?: string) { constructor(data: Data, id?: string) {
this.initialShape = TLDR.getShape(data, id || TLDR.getSelectedIds(data)[0]) const pageId = data.appState.currentPageId
this.initialShape = TLDR.getShape(data, id || TLDR.getSelectedIds(data, pageId)[0], pageId)
} }
start = (data: Data) => { start = (data: Data) => {
const pageId = data.appState.currentPageId
return { return {
document: { document: {
pageStates: { pageStates: {
[data.appState.currentPageId]: { [pageId]: {
editingId: this.initialShape.id, editingId: this.initialShape.id,
}, },
}, },
@ -25,12 +27,11 @@ export class TextSession implements Session {
} }
update = (data: Data, text: string) => { update = (data: Data, text: string) => {
const { const { initialShape } = this
initialShape: { id }, const pageId = data.appState.currentPageId
} = this
let nextShape: TextShape = { let nextShape: TextShape = {
...TLDR.getShape<TextShape>(data, id), ...TLDR.getShape<TextShape>(data, initialShape.id, pageId),
text, text,
} }
@ -42,9 +43,9 @@ export class TextSession implements Session {
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[id]: nextShape, [initialShape.id]: nextShape,
}, },
}, },
}, },
@ -53,21 +54,24 @@ export class TextSession implements Session {
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { const { initialShape } = this
initialShape: { id }, const pageId = data.appState.currentPageId
} = this
return { return {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[id]: TLDR.onSessionComplete(data, TLDR.getShape(data, id)), [initialShape.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id, pageId),
pageId
),
}, },
}, },
}, },
pageState: { pageState: {
[data.appState.currentPageId]: { [pageId]: {
editingId: undefined, editingId: undefined,
}, },
}, },
@ -77,8 +81,9 @@ export class TextSession implements Session {
complete(data: Data) { complete(data: Data) {
const { initialShape } = this const { initialShape } = this
const pageId = data.appState.currentPageId
const shape = TLDR.getShape<TextShape>(data, initialShape.id) const shape = TLDR.getShape<TextShape>(data, initialShape.id, pageId)
if (shape.text === initialShape.text) return undefined if (shape.text === initialShape.text) return undefined
@ -87,14 +92,14 @@ export class TextSession implements Session {
before: { before: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
}, },
}, },
pageState: { pageState: {
[data.appState.currentPageId]: { [pageId]: {
editingId: undefined, editingId: undefined,
}, },
}, },
@ -103,17 +108,18 @@ export class TextSession implements Session {
after: { after: {
document: { document: {
pages: { pages: {
[data.appState.currentPageId]: { [pageId]: {
shapes: { shapes: {
[initialShape.id]: TLDR.onSessionComplete( [initialShape.id]: TLDR.onSessionComplete(
data, data,
TLDR.getShape(data, initialShape.id) TLDR.getShape(data, initialShape.id, pageId),
pageId
), ),
}, },
}, },
}, },
pageState: { pageState: {
[data.appState.currentPageId]: { [pageId]: {
editingId: undefined, editingId: undefined,
}, },
}, },

View file

@ -1,6 +1,7 @@
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLBoundsCorner } from '@tldraw/core' import { TLBoundsCorner } from '@tldraw/core'
import { TLDrawStatus } from '~types'
describe('Transform single session', () => { describe('Transform single session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -12,8 +13,10 @@ describe('Transform single session', () => {
.startTransformSession([-10, -10], TLBoundsCorner.TopLeft) .startTransformSession([-10, -10], TLBoundsCorner.TopLeft)
.updateTransformSession([10, 10]) .updateTransformSession([10, 10])
.completeSession() .completeSession()
.undo()
.redo() expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
tlstate.undo().redo()
}) })
it('cancels session', () => { it('cancels session', () => {

View file

@ -35,7 +35,7 @@ export class TransformSingleSession implements Session {
const shapes = {} as Record<string, Partial<TLDrawShape>> const shapes = {} as Record<string, Partial<TLDrawShape>>
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id, data.appState.currentPageId)
const utils = TLDR.getShapeUtils(shape) const utils = TLDR.getShapeUtils(shape)
@ -95,7 +95,8 @@ export class TransformSingleSession implements Session {
beforeShapes[initialShape.id] = initialShape beforeShapes[initialShape.id] = initialShape
afterShapes[initialShape.id] = TLDR.onSessionComplete( afterShapes[initialShape.id] = TLDR.onSessionComplete(
data, data,
TLDR.getShape(data, initialShape.id) TLDR.getShape(data, initialShape.id, data.appState.currentPageId),
data.appState.currentPageId
) )
return { return {
@ -126,7 +127,11 @@ export function getTransformSingleSnapshot(
data: Data, data: Data,
transformType: TLBoundsEdge | TLBoundsCorner transformType: TLBoundsEdge | TLBoundsCorner
) { ) {
const shape = TLDR.getShape(data, TLDR.getSelectedIds(data)[0]) const shape = TLDR.getShape(
data,
TLDR.getSelectedIds(data, data.appState.currentPageId)[0],
data.appState.currentPageId
)
if (!shape) { if (!shape) {
throw Error('You must have one shape selected.') throw Error('You must have one shape selected.')

View file

@ -2,6 +2,7 @@ import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { TLBoundsCorner, Utils } from '@tldraw/core' import { TLBoundsCorner, Utils } from '@tldraw/core'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import { TLDrawStatus } from '~types'
function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) { function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
return Utils.getCommonBounds( return Utils.getCommonBounds(
@ -30,6 +31,8 @@ describe('Transform session', () => {
.updateTransformSession([10, 10]) .updateTransformSession([10, 10])
.completeSession() .completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({ expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
minX: 10, minX: 10,
minY: 10, minY: 10,

View file

@ -32,7 +32,7 @@ export class TransformSession implements Session {
const shapes = {} as Record<string, TLDrawShape> const shapes = {} as Record<string, TLDrawShape>
const pageState = TLDR.getPageState(data) const pageState = TLDR.getPageState(data, data.appState.currentPageId)
const newBoundingBox = Utils.getTransformedBoundingBox( const newBoundingBox = Utils.getTransformedBoundingBox(
initialBounds, initialBounds,
@ -56,13 +56,19 @@ export class TransformSession implements Session {
this.scaleY < 0 this.scaleY < 0
) )
shapes[id] = TLDR.transform(data, TLDR.getShape(data, id), newShapeBounds, { shapes[id] = TLDR.transform(
type: this.transformType, data,
initialShape, TLDR.getShape(data, id, data.appState.currentPageId),
scaleX: this.scaleX, newShapeBounds,
scaleY: this.scaleY, {
transformOrigin, type: this.transformType,
}) initialShape,
scaleX: this.scaleX,
scaleY: this.scaleY,
transformOrigin,
},
data.appState.currentPageId
)
}) })
return { return {
@ -104,7 +110,7 @@ export class TransformSession implements Session {
shapeBounds.forEach((shape) => { shapeBounds.forEach((shape) => {
beforeShapes[shape.id] = shape.initialShape beforeShapes[shape.id] = shape.initialShape
afterShapes[shape.id] = TLDR.getShape(data, shape.id) afterShapes[shape.id] = TLDR.getShape(data, shape.id, data.appState.currentPageId)
}) })
return { return {
@ -132,7 +138,7 @@ export class TransformSession implements Session {
} }
export function getTransformSnapshot(data: Data, transformType: TLBoundsEdge | TLBoundsCorner) { export function getTransformSnapshot(data: Data, transformType: TLBoundsEdge | TLBoundsCorner) {
const initialShapes = TLDR.getSelectedBranchSnapshot(data) const initialShapes = TLDR.getSelectedBranchSnapshot(data, data.appState.currentPageId)
const hasUnlockedShapes = initialShapes.length > 0 const hasUnlockedShapes = initialShapes.length > 0

View file

@ -1,7 +1,7 @@
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import { TLDrawState } from '~state' import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import type { TLDrawShape } from '~types' import { TLDrawShape, TLDrawStatus } from '~types'
describe('Brush session', () => { describe('Brush session', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
@ -17,6 +17,8 @@ describe('Brush session', () => {
tlstate.completeSession() tlstate.completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5]) expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
tlstate.undo() tlstate.undo()

View file

@ -1,14 +1,5 @@
import { TLPageState, Utils, Vec } from '@tldraw/core' import { TLPageState, Utils, Vec } from '@tldraw/core'
import { import { TLDrawShape, TLDrawBinding, Session, Data, Command, TLDrawStatus } from '~types'
TLDrawShape,
TLDrawBinding,
PagePartial,
Session,
Data,
Command,
TLDrawStatus,
ShapesWithProp,
} from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class TranslateSession implements Session { export class TranslateSession implements Session {
@ -92,7 +83,8 @@ export class TranslateSession implements Session {
// Either way, move the clones // Either way, move the clones
clones.forEach((shape) => { clones.forEach((shape) => {
const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id) const current =
nextShapes[shape.id] || TLDR.getShape(data, shape.id, data.appState.currentPageId)
if (!current.point) throw Error('No point on that clone!') if (!current.point) throw Error('No point on that clone!')
@ -129,7 +121,8 @@ export class TranslateSession implements Session {
// Move the shapes by the delta // Move the shapes by the delta
initialShapes.forEach((shape) => { initialShapes.forEach((shape) => {
const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id) const current =
nextShapes[shape.id] || TLDR.getShape(data, shape.id, data.appState.currentPageId)
if (!current.point) throw Error('No point on that clone!') if (!current.point) throw Error('No point on that clone!')
@ -147,9 +140,9 @@ export class TranslateSession implements Session {
shapes: nextShapes, shapes: nextShapes,
bindings: nextBindings, bindings: nextBindings,
}, },
pageStates: { },
[data.appState.currentPageId]: nextPageState, pageStates: {
}, [data.appState.currentPageId]: nextPageState,
}, },
}, },
} }
@ -183,15 +176,17 @@ export class TranslateSession implements Session {
shapes: nextShapes, shapes: nextShapes,
bindings: nextBindings, bindings: nextBindings,
}, },
pageStates: { },
[data.appState.currentPageId]: nextPageState, pageStates: {
}, [data.appState.currentPageId]: nextPageState,
}, },
}, },
} }
} }
complete(data: Data): Command { complete(data: Data): Command {
const pageId = data.appState.currentPageId
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot
const beforeBindings: Record<string, Partial<TLDrawBinding> | undefined> = {} const beforeBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
@ -202,17 +197,17 @@ export class TranslateSession implements Session {
clones.forEach((shape) => { clones.forEach((shape) => {
beforeShapes[shape.id] = undefined beforeShapes[shape.id] = undefined
afterShapes[shape.id] = this.isCloning ? TLDR.getShape(data, shape.id) : undefined afterShapes[shape.id] = this.isCloning ? TLDR.getShape(data, shape.id, pageId) : undefined
}) })
initialShapes.forEach((shape) => { initialShapes.forEach((shape) => {
beforeShapes[shape.id] = { point: shape.point } beforeShapes[shape.id] = { point: shape.point }
afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id).point } afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id, pageId).point }
}) })
clonedBindings.forEach((binding) => { clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id) afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId)
}) })
bindingsToDelete.forEach((binding) => { bindingsToDelete.forEach((binding) => {
@ -220,7 +215,7 @@ export class TranslateSession implements Session {
for (const id of [binding.toId, binding.fromId]) { for (const id of [binding.toId, binding.fromId]) {
// Let's also look at the bound shape... // Let's also look at the bound shape...
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id, pageId)
// If the bound shape has a handle that references the deleted binding, delete that reference // If the bound shape has a handle that references the deleted binding, delete that reference
if (!shape.handles) continue if (!shape.handles) continue
@ -279,7 +274,7 @@ export class TranslateSession implements Session {
}, },
pageStates: { pageStates: {
[data.appState.currentPageId]: { [data.appState.currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data)], selectedIds: [...TLDR.getSelectedIds(data, data.appState.currentPageId)],
}, },
}, },
}, },
@ -290,18 +285,18 @@ export class TranslateSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTranslateSnapshot(data: Data) { export function getTranslateSnapshot(data: Data) {
const selectedShapes = TLDR.getSelectedShapeSnapshot(data) const selectedShapes = TLDR.getSelectedShapeSnapshot(data, data.appState.currentPageId)
const hasUnlockedShapes = selectedShapes.length > 0 const hasUnlockedShapes = selectedShapes.length > 0
const cloneMap: Record<string, string> = {} const cloneMap: Record<string, string> = {}
const page = TLDR.getPage(data) const page = TLDR.getPage(data, data.appState.currentPageId)
const initialParents = Array.from(new Set(selectedShapes.map((s) => s.parentId)).values()) const initialParents = Array.from(new Set(selectedShapes.map((s) => s.parentId)).values())
.filter((id) => id !== page.id) .filter((id) => id !== page.id)
.map((id) => { .map((id) => {
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id, data.appState.currentPageId)
return { return {
id, id,
children: shape.children, children: shape.children,
@ -315,7 +310,7 @@ export function getTranslateSnapshot(data: Data) {
...shape, ...shape,
id: Utils.uniqueId(), id: Utils.uniqueId(),
parentId: shape.parentId, parentId: shape.parentId,
childIndex: TLDR.getChildIndexAbove(data, shape.id), childIndex: TLDR.getChildIndexAbove(data, shape.id, data.appState.currentPageId),
} }
cloneMap[shape.id] = clone.id cloneMap[shape.id] = clone.id
@ -343,11 +338,12 @@ export function getTranslateSnapshot(data: Data) {
} }
}) })
const selectedIds = TLDR.getSelectedIds(data) const selectedIds = TLDR.getSelectedIds(data, data.appState.currentPageId)
const bindingsToDelete = TLDR.getRelatedBindings( const bindingsToDelete = TLDR.getRelatedBindings(
data, data,
selectedShapes.filter((shape) => shape.handles !== undefined).map((shape) => shape.id) selectedShapes.filter((shape) => shape.handles !== undefined).map((shape) => shape.id),
data.appState.currentPageId
) )
return { return {

View file

@ -16,14 +16,14 @@ export class TLDR {
return getShapeUtils(typeof shape === 'string' ? ({ type: shape } as T) : shape) return getShapeUtils(typeof shape === 'string' ? ({ type: shape } as T) : shape)
} }
static getSelectedShapes(data: Data) { static getSelectedShapes(data: Data, pageId: string) {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const selectedIds = this.getSelectedIds(data) const selectedIds = this.getSelectedIds(data, pageId)
return selectedIds.map((id) => page.shapes[id]) return selectedIds.map((id) => page.shapes[id])
} }
static screenToWorld(data: Data, point: number[]) { static screenToWorld(data: Data, point: number[]) {
const camera = this.getCamera(data) const camera = this.getPageState(data, data.appState.currentPageId).camera
return Vec.sub(Vec.div(point, camera.zoom), camera.point) return Vec.sub(Vec.div(point, camera.zoom), camera.point)
} }
@ -45,30 +45,30 @@ export class TLDR {
return Utils.clamp(zoom, 0.1, 5) return Utils.clamp(zoom, 0.1, 5)
} }
static getPage(data: Data, pageId = data.appState.currentPageId): TLDrawPage { static getPage(data: Data, pageId: string): TLDrawPage {
return data.document.pages[pageId] return data.document.pages[pageId]
} }
static getPageState(data: Data, pageId = data.appState.currentPageId): TLPageState { static getPageState(data: Data, pageId: string): TLPageState {
return data.document.pageStates[pageId] return data.document.pageStates[pageId]
} }
static getSelectedIds(data: Data, pageId = data.appState.currentPageId): string[] { static getSelectedIds(data: Data, pageId: string): string[] {
return this.getPageState(data, pageId).selectedIds return this.getPageState(data, pageId).selectedIds
} }
static getShapes(data: Data, pageId = data.appState.currentPageId): TLDrawShape[] { static getShapes(data: Data, pageId: string): TLDrawShape[] {
return Object.values(this.getPage(data, pageId).shapes) return Object.values(this.getPage(data, pageId).shapes)
} }
static getCamera(data: Data, pageId = data.appState.currentPageId): TLPageState['camera'] { static getCamera(data: Data, pageId: string): TLPageState['camera'] {
return this.getPageState(data, pageId).camera return this.getPageState(data, pageId).camera
} }
static getShape<T extends TLDrawShape = TLDrawShape>( static getShape<T extends TLDrawShape = TLDrawShape>(
data: Data, data: Data,
shapeId: string, shapeId: string,
pageId = data.appState.currentPageId pageId: string
): T { ): T {
return this.getPage(data, pageId).shapes[shapeId] as T return this.getPage(data, pageId).shapes[shapeId] as T
} }
@ -83,41 +83,43 @@ export class TLDR {
static getSelectedBounds(data: Data): TLBounds { static getSelectedBounds(data: Data): TLBounds {
return Utils.getCommonBounds( return Utils.getCommonBounds(
this.getSelectedShapes(data).map((shape) => getShapeUtils(shape).getBounds(shape)) this.getSelectedShapes(data, data.appState.currentPageId).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
) )
} }
static getParentId(data: Data, id: string) { static getParentId(data: Data, id: string, pageId: string) {
return this.getShape(data, id).parentId return this.getShape(data, id, pageId).parentId
} }
static getPointedId(data: Data, id: string): string { static getPointedId(data: Data, id: string, pageId: string): string {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const pageState = this.getPageState(data) const pageState = this.getPageState(data, data.appState.currentPageId)
const shape = this.getShape(data, id) const shape = this.getShape(data, id, pageId)
if (!shape) return id if (!shape) return id
return shape.parentId === pageState.currentParentId || shape.parentId === page.id return shape.parentId === pageState.currentParentId || shape.parentId === page.id
? id ? id
: this.getPointedId(data, shape.parentId) : this.getPointedId(data, shape.parentId, pageId)
} }
static getDrilledPointedId(data: Data, id: string): string { static getDrilledPointedId(data: Data, id: string, pageId: string): string {
const shape = this.getShape(data, id) const shape = this.getShape(data, id, pageId)
const { currentPageId } = data.appState const { currentPageId } = data.appState
const { currentParentId, pointedId } = this.getPageState(data) const { currentParentId, pointedId } = this.getPageState(data, data.appState.currentPageId)
return shape.parentId === currentPageId || return shape.parentId === currentPageId ||
shape.parentId === pointedId || shape.parentId === pointedId ||
shape.parentId === currentParentId shape.parentId === currentParentId
? id ? id
: this.getDrilledPointedId(data, shape.parentId) : this.getDrilledPointedId(data, shape.parentId, pageId)
} }
static getTopParentId(data: Data, id: string): string { static getTopParentId(data: Data, id: string, pageId: string): string {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const pageState = this.getPageState(data) const pageState = this.getPageState(data, pageId)
const shape = this.getShape(data, id) const shape = this.getShape(data, id, pageId)
if (shape.parentId === shape.id) { if (shape.parentId === shape.id) {
throw Error(`Shape has the same id as its parent! ${shape.id}`) throw Error(`Shape has the same id as its parent! ${shape.id}`)
@ -125,32 +127,37 @@ export class TLDR {
return shape.parentId === page.id || shape.parentId === pageState.currentParentId return shape.parentId === page.id || shape.parentId === pageState.currentParentId
? id ? id
: this.getTopParentId(data, shape.parentId) : this.getTopParentId(data, shape.parentId, pageId)
} }
// Get an array of a shape id and its descendant shapes' ids // Get an array of a shape id and its descendant shapes' ids
static getDocumentBranch(data: Data, id: string): string[] { static getDocumentBranch(data: Data, id: string, pageId: string): string[] {
const shape = this.getShape(data, id) const shape = this.getShape(data, id, pageId)
if (shape.children === undefined) return [id] if (shape.children === undefined) return [id]
return [id, ...shape.children.flatMap((childId) => this.getDocumentBranch(data, childId))] return [
id,
...shape.children.flatMap((childId) => this.getDocumentBranch(data, childId, pageId)),
]
} }
// Get a deep array of unproxied shapes and their descendants // Get a deep array of unproxied shapes and their descendants
static getSelectedBranchSnapshot<K>( static getSelectedBranchSnapshot<K>(
data: Data, data: Data,
pageId: string,
fn: (shape: TLDrawShape) => K fn: (shape: TLDrawShape) => K
): ({ id: string } & K)[] ): ({ id: string } & K)[]
static getSelectedBranchSnapshot(data: Data): TLDrawShape[] static getSelectedBranchSnapshot(data: Data, pageId: string): TLDrawShape[]
static getSelectedBranchSnapshot<K>( static getSelectedBranchSnapshot<K>(
data: Data, data: Data,
pageId: string,
fn?: (shape: TLDrawShape) => K fn?: (shape: TLDrawShape) => K
): (TLDrawShape | K)[] { ): (TLDrawShape | K)[] {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const copies = this.getSelectedIds(data) const copies = this.getSelectedIds(data, pageId)
.flatMap((id) => this.getDocumentBranch(data, id).map((id) => page.shapes[id])) .flatMap((id) => this.getDocumentBranch(data, id, pageId).map((id) => page.shapes[id]))
.filter((shape) => !shape.isLocked) .filter((shape) => !shape.isLocked)
.map(Utils.deepClone) .map(Utils.deepClone)
@ -162,16 +169,18 @@ export class TLDR {
} }
// Get a shallow array of unproxied shapes // Get a shallow array of unproxied shapes
static getSelectedShapeSnapshot(data: Data): TLDrawShape[] static getSelectedShapeSnapshot(data: Data, pageId: string): TLDrawShape[]
static getSelectedShapeSnapshot<K>( static getSelectedShapeSnapshot<K>(
data: Data, data: Data,
pageId: string,
fn?: (shape: TLDrawShape) => K fn?: (shape: TLDrawShape) => K
): ({ id: string } & K)[] ): ({ id: string } & K)[]
static getSelectedShapeSnapshot<K>( static getSelectedShapeSnapshot<K>(
data: Data, data: Data,
pageId: string,
fn?: (shape: TLDrawShape) => K fn?: (shape: TLDrawShape) => K
): (TLDrawShape | K)[] { ): (TLDrawShape | K)[] {
const copies = this.getSelectedShapes(data) const copies = this.getSelectedShapes(data, pageId)
.filter((shape) => !shape.isLocked) .filter((shape) => !shape.isLocked)
.map(Utils.deepClone) .map(Utils.deepClone)
@ -184,8 +193,8 @@ 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. // 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. // Use this to decide which shapes to clone as before / after for a command.
static getAllEffectedShapeIds(data: Data, ids: string[]): string[] { static getAllEffectedShapeIds(data: Data, ids: string[], pageId: string): string[] {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const visited = new Set(ids) const visited = new Set(ids)
@ -232,9 +241,10 @@ export class TLDR {
data: Data, data: Data,
id: string, id: string,
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {},
pageId: string
): Data { ): Data {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const shape = page.shapes[id] as T const shape = page.shapes[id] as T
if (shape.children !== undefined) { if (shape.children !== undefined) {
@ -246,8 +256,8 @@ export class TLDR {
if (deltas) { if (deltas) {
return deltas.reduce<Data>((cData, delta) => { return deltas.reduce<Data>((cData, delta) => {
if (!delta.id) throw Error('Delta must include an id!') if (!delta.id) throw Error('Delta must include an id!')
const cPage = this.getPage(cData) const cPage = this.getPage(cData, pageId)
const deltaShape = this.getShape(cData, delta.id) const deltaShape = this.getShape(cData, delta.id, pageId)
if (!beforeShapes[delta.id]) { if (!beforeShapes[delta.id]) {
beforeShapes[delta.id] = deltaShape beforeShapes[delta.id] = deltaShape
@ -256,7 +266,7 @@ export class TLDR {
afterShapes[delta.id] = cPage.shapes[delta.id] afterShapes[delta.id] = cPage.shapes[delta.id]
if (deltaShape.children !== undefined) { if (deltaShape.children !== undefined) {
this.recursivelyUpdateChildren(cData, delta.id, beforeShapes, afterShapes) this.recursivelyUpdateChildren(cData, delta.id, beforeShapes, afterShapes, pageId)
} }
return cData return cData
@ -271,23 +281,24 @@ export class TLDR {
data: Data, data: Data,
id: string, id: string,
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {},
pageId: string
): Data { ): Data {
const page = { ...this.getPage(data) } const page = { ...this.getPage(data, pageId) }
const shape = this.getShape<T>(data, id) const shape = this.getShape<T>(data, id, pageId)
if (page.id === 'doc') { if (page.id === 'doc') {
throw Error('wtf') throw Error('wtf')
} }
if (shape.parentId !== page.id) { if (shape.parentId !== page.id) {
const parent = this.getShape(data, shape.parentId) const parent = this.getShape(data, shape.parentId, pageId)
if (!parent.children) throw Error('No children in parent!') if (!parent.children) throw Error('No children in parent!')
const delta = this.getShapeUtils(parent).onChildrenChange( const delta = this.getShapeUtils(parent).onChildrenChange(
parent, parent,
parent.children.map((childId) => this.getShape(data, childId)) parent.children.map((childId) => this.getShape(data, childId, pageId))
) )
if (delta) { if (delta) {
@ -299,7 +310,13 @@ export class TLDR {
} }
if (parent.parentId !== page.id) { if (parent.parentId !== page.id) {
return this.recursivelyUpdateParents(data, parent.parentId, beforeShapes, afterShapes) return this.recursivelyUpdateParents(
data,
parent.parentId,
beforeShapes,
afterShapes,
pageId
)
} }
} }
@ -323,36 +340,40 @@ export class TLDR {
data: Data, data: Data,
id: string, id: string,
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {},
pageId: string
): Data { ): Data {
const page = { ...this.getPage(data) } const page = { ...this.getPage(data, pageId) }
return Object.values(page.bindings) return Object.values(page.bindings)
.filter((binding) => binding.fromId === id || binding.toId === id) .filter((binding) => binding.fromId === id || binding.toId === id)
.reduce((cData, binding) => { .reduce((cData, binding) => {
if (!beforeShapes[binding.id]) { if (!beforeShapes[binding.id]) {
beforeShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId)) beforeShapes[binding.fromId] = Utils.deepClone(
this.getShape(cData, binding.fromId, pageId)
)
} }
if (!beforeShapes[binding.toId]) { if (!beforeShapes[binding.toId]) {
beforeShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId)) beforeShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId, pageId))
} }
this.onBindingChange( this.onBindingChange(
cData, cData,
this.getShape(cData, binding.fromId), this.getShape(cData, binding.fromId, pageId),
binding, binding,
this.getShape(cData, binding.toId) this.getShape(cData, binding.toId, pageId),
pageId
) )
afterShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId)) afterShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId, pageId))
afterShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId)) afterShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId, pageId))
return cData return cData
}, data) }, data)
} }
static getChildIndexAbove(data: Data, id: string): number { static getChildIndexAbove(data: Data, id: string, pageId: string): number {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const shape = page.shapes[id] const shape = page.shapes[id]
@ -387,7 +408,7 @@ export class TLDR {
data: Data, data: Data,
ids: string[], ids: string[],
fn: (shape: T, i: number) => Partial<T>, fn: (shape: T, i: number) => Partial<T>,
pageId = data.appState.currentPageId pageId: string
): { ): {
before: Record<string, Partial<T>> before: Record<string, Partial<T>>
after: Record<string, Partial<T>> after: Record<string, Partial<T>>
@ -408,15 +429,15 @@ export class TLDR {
}) })
const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => { const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => {
return this.recursivelyUpdateChildren(cData, id, beforeShapes, afterShapes) return this.recursivelyUpdateChildren(cData, id, beforeShapes, afterShapes, pageId)
}, data) }, data)
const dataWithParentChanges = ids.reduce<Data>((cData, id) => { const dataWithParentChanges = ids.reduce<Data>((cData, id) => {
return this.recursivelyUpdateParents(cData, id, beforeShapes, afterShapes) return this.recursivelyUpdateParents(cData, id, beforeShapes, afterShapes, pageId)
}, dataWithChildrenChanges) }, dataWithChildrenChanges)
const dataWithBindingChanges = ids.reduce<Data>((cData, id) => { const dataWithBindingChanges = ids.reduce<Data>((cData, id) => {
return this.updateBindings(cData, id, beforeShapes, afterShapes) return this.updateBindings(cData, id, beforeShapes, afterShapes, pageId)
}, dataWithParentChanges) }, dataWithParentChanges)
return { return {
@ -429,7 +450,7 @@ export class TLDR {
static createShapes( static createShapes(
data: Data, data: Data,
shapes: TLDrawShape[], shapes: TLDrawShape[],
pageId = data.appState.currentPageId pageId: string
): { before: DeepPartial<Data>; after: DeepPartial<Data> } { ): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const before: DeepPartial<Data> = { const before: DeepPartial<Data> = {
document: { document: {
@ -496,8 +517,10 @@ export class TLDR {
static deleteShapes( static deleteShapes(
data: Data, data: Data,
shapes: TLDrawShape[] | string[], shapes: TLDrawShape[] | string[],
pageId = data.appState.currentPageId pageId?: string
): { before: DeepPartial<Data>; after: DeepPartial<Data> } { ): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
pageId = pageId ? pageId : data.appState.currentPageId
const page = this.getPage(data, pageId) const page = this.getPage(data, pageId)
const shapeIds = const shapeIds =
@ -583,28 +606,29 @@ export class TLDR {
} }
} }
static onSessionComplete<T extends TLDrawShape>(data: Data, shape: T) { static onSessionComplete<T extends TLDrawShape>(data: Data, shape: T, pageId: string) {
const delta = getShapeUtils(shape).onSessionComplete(shape) const delta = getShapeUtils(shape).onSessionComplete(shape)
if (!delta) return shape if (!delta) return shape
return this.mutate(data, shape, delta) return this.mutate(data, shape, delta, pageId)
} }
static onChildrenChange<T extends TLDrawShape>(data: Data, shape: T) { static onChildrenChange<T extends TLDrawShape>(data: Data, shape: T, pageId: string) {
if (!shape.children) return if (!shape.children) return
const delta = getShapeUtils(shape).onChildrenChange( const delta = getShapeUtils(shape).onChildrenChange(
shape, shape,
shape.children.map((id) => this.getShape(data, id)) shape.children.map((id) => this.getShape(data, id, pageId))
) )
if (!delta) return shape if (!delta) return shape
return this.mutate(data, shape, delta) return this.mutate(data, shape, delta, pageId)
} }
static onBindingChange<T extends TLDrawShape>( static onBindingChange<T extends TLDrawShape>(
data: Data, data: Data,
shape: T, shape: T,
binding: TLDrawBinding, binding: TLDrawBinding,
otherShape: TLDrawShape otherShape: TLDrawShape,
pageId: string
) { ) {
const delta = getShapeUtils(shape).onBindingChange( const delta = getShapeUtils(shape).onBindingChange(
shape, shape,
@ -614,32 +638,39 @@ export class TLDR {
getShapeUtils(otherShape).getCenter(otherShape) getShapeUtils(otherShape).getCenter(otherShape)
) )
if (!delta) return shape if (!delta) return shape
return this.mutate(data, shape, delta) return this.mutate(data, shape, delta, pageId)
} }
static transform<T extends TLDrawShape>( static transform<T extends TLDrawShape>(
data: Data, data: Data,
shape: T, shape: T,
bounds: TLBounds, bounds: TLBounds,
info: TLTransformInfo<T> info: TLTransformInfo<T>,
pageId: string
) { ) {
return this.mutate(data, shape, getShapeUtils(shape).transform(shape, bounds, info)) return this.mutate(data, shape, getShapeUtils(shape).transform(shape, bounds, info), pageId)
} }
static transformSingle<T extends TLDrawShape>( static transformSingle<T extends TLDrawShape>(
data: Data, data: Data,
shape: T, shape: T,
bounds: TLBounds, bounds: TLBounds,
info: TLTransformInfo<T> info: TLTransformInfo<T>,
pageId: string
) { ) {
return this.mutate(data, shape, getShapeUtils(shape).transformSingle(shape, bounds, info)) return this.mutate(
data,
shape,
getShapeUtils(shape).transformSingle(shape, bounds, info),
pageId
)
} }
static mutate<T extends TLDrawShape>(data: Data, shape: T, props: Partial<T>) { static mutate<T extends TLDrawShape>(data: Data, shape: T, props: Partial<T>, pageId: string) {
let next = getShapeUtils(shape).mutate(shape, props) let next = getShapeUtils(shape).mutate(shape, props)
if (props.children) { if (props.children) {
next = this.onChildrenChange(data, next) || next next = this.onChildrenChange(data, next, pageId) || next
} }
// data.page.shapes[next.id] = next // data.page.shapes[next.id] = next
@ -651,12 +682,12 @@ export class TLDR {
/* Parents */ /* Parents */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static updateParents(data: Data, changedShapeIds: string[]): void { static updateParents(data: Data, pageId: string, changedShapeIds: string[]): void {
const page = this.getPage(data) const page = this.getPage(data, pageId)
if (changedShapeIds.length === 0) return if (changedShapeIds.length === 0) return
const { shapes } = this.getPage(data) const { shapes } = this.getPage(data, pageId)
const parentToUpdateIds = Array.from( const parentToUpdateIds = Array.from(
new Set(changedShapeIds.map((id) => shapes[id].parentId).values()) new Set(changedShapeIds.map((id) => shapes[id].parentId).values())
@ -669,19 +700,17 @@ export class TLDR {
throw Error('A shape is parented to a shape without a children array.') throw Error('A shape is parented to a shape without a children array.')
} }
this.onChildrenChange(data, parent) this.onChildrenChange(data, parent, pageId)
} }
this.updateParents(data, parentToUpdateIds) this.updateParents(data, pageId, parentToUpdateIds)
} }
static getSelectedStyle(data: Data): ShapeStyles | false { static getSelectedStyle(data: Data, pageId: string): ShapeStyles | false {
const { const { currentStyle } = data.appState
appState: { currentStyle },
} = data
const page = this.getPage(data) const page = data.document.pages[pageId]
const pageState = this.getPageState(data) const pageState = data.document.pageStates[pageId]
if (pageState.selectedIds.length === 0) { if (pageState.selectedIds.length === 0) {
return currentStyle return currentStyle
@ -718,36 +747,36 @@ export class TLDR {
/* Bindings */ /* Bindings */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static getBinding(data: Data, id: string, pageId = data.appState.currentPageId): TLDrawBinding { static getBinding(data: Data, id: string, pageId: string): TLDrawBinding {
return this.getPage(data, pageId).bindings[id] return this.getPage(data, pageId).bindings[id]
} }
static getBindings(data: Data, pageId = data.appState.currentPageId): TLDrawBinding[] { static getBindings(data: Data, pageId: string): TLDrawBinding[] {
const page = this.getPage(data, pageId) const page = this.getPage(data, pageId)
return Object.values(page.bindings) return Object.values(page.bindings)
} }
static getBindableShapeIds(data: Data) { static getBindableShapeIds(data: Data) {
return this.getShapes(data) return this.getShapes(data, data.appState.currentPageId)
.filter((shape) => TLDR.getShapeUtils(shape).canBind) .filter((shape) => TLDR.getShapeUtils(shape).canBind)
.sort((a, b) => b.childIndex - a.childIndex) .sort((a, b) => b.childIndex - a.childIndex)
.map((shape) => shape.id) .map((shape) => shape.id)
} }
static getBindingsWithShapeIds(data: Data, ids: string[]): TLDrawBinding[] { static getBindingsWithShapeIds(data: Data, ids: string[], pageId: string): TLDrawBinding[] {
return Array.from( return Array.from(
new Set( new Set(
this.getBindings(data).filter((binding) => { this.getBindings(data, pageId).filter((binding) => {
return ids.includes(binding.toId) || ids.includes(binding.fromId) return ids.includes(binding.toId) || ids.includes(binding.fromId)
}) })
).values() ).values()
) )
} }
static getRelatedBindings(data: Data, ids: string[]): TLDrawBinding[] { static getRelatedBindings(data: Data, ids: string[], pageId: string): TLDrawBinding[] {
const changedShapeIds = new Set(ids) const changedShapeIds = new Set(ids)
const page = this.getPage(data) const page = this.getPage(data, pageId)
// Find all bindings that we need to update // Find all bindings that we need to update
const bindingsArr = Object.values(page.bindings) const bindingsArr = Object.values(page.bindings)

View file

@ -28,6 +28,22 @@ describe('TLDrawState', () => {
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1) expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
}) })
it('pastes a shape to a new page', () => {
tlstate.loadDocument(mockDocument)
tlstate.deselectAll().copy(['rect1']).createPage().paste()
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
tlstate.undo()
expect(Object.keys(tlstate.page.shapes).length).toBe(0)
tlstate.redo()
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
})
}) })
describe('Selection', () => { describe('Selection', () => {

File diff suppressed because it is too large Load diff

View file

@ -95,5 +95,5 @@ export default function Editor(): JSX.Element {
return <div /> return <div />
} }
return <TLDraw document={initialDoc} onChange={handleChange} /> return <TLDraw document={value} onChange={handleChange} />
} }

21077
tsconfig.tsbuildinfo Normal file

File diff suppressed because it is too large Load diff