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`
@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 {
--tl-zoom: 1;
--tl-scale: calc(1 / var(--tl-zoom));

View file

@ -102,5 +102,5 @@ export default function Editor(): JSX.Element {
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 */
import * as React from 'react'
import { openDB, DBSchema } from 'idb'
import { openDB, DBSchema, deleteDB } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw'
const VERSION = 1
@ -57,6 +57,8 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state.
React.useEffect(() => {
async function handleLoad() {
await deleteDB('db')
const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) {
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'
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 {

View file

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

View file

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

View file

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

View file

@ -17,12 +17,16 @@ describe('Change page command', () => {
expect(tlstate.page.id).toBe(initialId)
tlstate.undo()
tlstate.changePage(nextId)
expect(tlstate.page.id).toBe(nextId)
tlstate.redo()
tlstate.undo()
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 { TLDR } from '~state/tldr'
export function changePage(data: Data): Command {
export function changePage(data: Data, pageId: string): Command {
return {
id: 'create_page',
before: {},
after: {},
id: 'change_page',
before: {
appState: {
currentPageId: data.appState.currentPageId,
},
},
after: {
appState: {
currentPageId: pageId,
},
},
}
}

View file

@ -8,19 +8,27 @@ describe('Create page command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
const initialPageState = tlstate.pageState
tlstate.createPage()
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.pageState).toEqual(nextPageState)
tlstate.undo()
expect(Object.keys(tlstate.document.pages).length).toBe(1)
expect(tlstate.page.id).toBe(initialId)
expect(tlstate.pageState).toEqual(initialPageState)
tlstate.redo()
expect(Object.keys(tlstate.document.pages).length).toBe(2)
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 { TLDR } from '~state/tldr'
import type { Data, Command } from '~types'
import { Utils } from '@tldraw/core'
export function createPage(data: Data): Command {
const newId = Utils.uniqueId()
const { currentPageId } = data.appState
return {
id: 'create_page',
before: {},
after: {},
before: {
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'
export function create(data: Data, shapes: TLDrawShape[]): Command {
const { currentPageId } = data.appState
const beforeShapes: 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: {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: beforeShapes,
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data)],
[currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data, currentPageId)],
},
},
},
@ -30,12 +31,12 @@ export function create(data: Data, shapes: TLDrawShape[]): Command {
after: {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: afterShapes,
},
},
pageStates: {
[data.appState.currentPageId]: {
[currentPageId]: {
selectedIds: shapes.map((shape) => shape.id),
},
},

View file

@ -7,22 +7,22 @@ describe('Delete page', () => {
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
const initialId = tlstate.currentPageId
tlstate.createPage()
const nextId = tlstate.page.id
const nextId = tlstate.currentPageId
tlstate.deletePage()
expect(tlstate.page.id).toBe(nextId)
expect(tlstate.currentPageId).toBe(initialId)
tlstate.undo()
expect(tlstate.page.id).toBe(initialId)
expect(tlstate.currentPageId).toBe(nextId)
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 { TLDR } from '~state/tldr'
import type { Data, Command } from '~types'
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 {
id: 'delete_page',
before: {},
after: {},
before: {
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
export function deleteShapes(data: Data, ids: string[]): Command {
const { currentPageId } = data.appState
const before: PagePartial = {
shapes: {},
bindings: {},
@ -18,11 +20,11 @@ export function deleteShapes(data: Data, ids: string[]): Command {
// These are the shapes we're definitely going to delete
ids.forEach((id) => {
before.shapes[id] = TLDR.getShape(data, id)
before.shapes[id] = TLDR.getShape(data, id, currentPageId)
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
Object.values(page.bindings).forEach((binding) => {
@ -34,7 +36,7 @@ export function deleteShapes(data: Data, ids: string[]): Command {
after.bindings[binding.id] = undefined
// 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 (shape.handles) {
@ -60,20 +62,20 @@ export function deleteShapes(data: Data, ids: string[]): Command {
before: {
document: {
pages: {
[data.appState.currentPageId]: before,
[currentPageId]: before,
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: TLDR.getSelectedIds(data) },
[currentPageId]: { selectedIds: TLDR.getSelectedIds(data, currentPageId) },
},
},
},
after: {
document: {
pages: {
[data.appState.currentPageId]: after,
[currentPageId]: after,
},
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'
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 { before, after } = TLDR.mutateShapes(data, ids, (shape) => {
if (!deltaMap[shape.id]) return shape
return { point: deltaMap[shape.id].next }
})
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
if (!deltaMap[shape.id]) return shape
return { point: deltaMap[shape.id].next }
},
currentPageId
)
return {
id: 'distribute_shapes',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
},
},
after: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: after },
[currentPageId]: { shapes: after },
},
},
},

View file

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

View file

@ -3,11 +3,12 @@ import { TLDR } from '~state/tldr'
import type { Data, Command } from '~types'
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(
TLDR.getSelectedIds(data)
.map((id) => TLDR.getShape(data, id))
TLDR.getSelectedIds(data, currentPageId)
.map((id) => TLDR.getShape(data, id, currentPageId))
.map((shape) => {
const id = Utils.uniqueId()
return [
@ -28,20 +29,20 @@ export function duplicate(data: Data, ids: string[]): Command {
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: ids },
[currentPageId]: { selectedIds: ids },
},
},
},
after: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: after },
[currentPageId]: { shapes: after },
},
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'
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 commonBounds = Utils.getCommonBounds(boundsForShapes)
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => {
const shapeBounds = TLDR.getBounds(shape)
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
const shapeBounds = TLDR.getBounds(shape)
switch (type) {
case FlipType.Horizontal: {
const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
commonBounds,
commonBounds,
shapeBounds,
true,
false
)
switch (type) {
case FlipType.Horizontal: {
const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
commonBounds,
commonBounds,
shapeBounds,
true,
false
)
return TLDR.getShapeUtils(shape).transform(shape, newShapeBounds, {
type: TLBoundsCorner.TopLeft,
scaleX: -1,
scaleY: 1,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
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(
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(
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],
})
}
}
})
},
currentPageId
)
return {
id: 'flip_shapes',

View file

@ -11,3 +11,8 @@ export * from './toggle'
export * from './translate'
export * from './flip'
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']
function getSortedShapeIds(data: Data) {
return TLDR.getShapes(data)
return TLDR.getShapes(data, data.appState.currentPageId)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id)
.join('')

View file

@ -2,8 +2,10 @@ import { MoveType, Data, TLDrawShape, Command } from '~types'
import { TLDR } from '~state/tldr'
export function move(data: Data, ids: string[], type: MoveType): Command {
const { currentPageId } = data.appState
// 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: {
before: Record<string, Partial<TLDrawShape>>
@ -14,7 +16,7 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
let startChildIndex: 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
Array.from(parentIds.values()).forEach((parentId) => {
@ -22,11 +24,11 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
if (parentId === page.id) {
sortedChildren = Object.values(page.shapes).sort((a, b) => a.childIndex - b.childIndex)
} else {
const parent = TLDR.getShape(data, parentId)
const parent = TLDR.getShape(data, parentId, currentPageId)
if (!parent.children) throw Error('No children in parent!')
sortedChildren = parent.children
.map((childId) => TLDR.getShape(data, childId))
.map((childId) => TLDR.getShape(data, childId, currentPageId))
.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(),
(_shape, i) => ({
childIndex: startChildIndex - (i + 1) * step,
})
}),
currentPageId
)
break
@ -95,7 +98,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id),
(_shape, i) => ({
childIndex: startChildIndex + (i + 1),
})
}),
currentPageId
)
break
@ -140,7 +144,8 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
sortedIndicesToMove.map((i) => sortedChildren[i].id),
(shape) => ({
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),
(shape) => ({
childIndex: indexMap[shape.id],
})
}),
currentPageId
)
}

View file

@ -1,10 +1,22 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
import type { Data, Command } from '~types'
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 {
id: 'edit_page',
before: {},
after: {},
id: 'rename_page',
before: {
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
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 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 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 {
id: 'toggle_shapes',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
pageStates: {
[data.appState.currentPageId]: {
boundsRotation: prevBoundsRotation,
},
[currentPageId]: { boundsRotation: prevBoundsRotation },
},
},
},
after: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: after },
[currentPageId]: { shapes: after },
},
pageStates: {
[data.appState.currentPageId]: {
boundsRotation: nextBoundsRotation,
},
[currentPageId]: { boundsRotation: nextBoundsRotation },
},
},
},

View file

@ -4,64 +4,71 @@ import type { Data, Command } from '~types'
import { TLDR } from '~state/tldr'
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 commonBounds = Utils.getCommonBounds(boundsForShapes)
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => {
const bounds = TLDR.getBounds(shape)
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
const bounds = TLDR.getBounds(shape)
switch (type) {
case StretchType.Horizontal: {
const newBounds = {
...bounds,
minX: commonBounds.minX,
maxX: commonBounds.maxX,
width: commonBounds.width,
switch (type) {
case StretchType.Horizontal: {
const newBounds = {
...bounds,
minX: commonBounds.minX,
maxX: commonBounds.maxX,
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, {
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, {
type: TLBoundsCorner.TopLeft,
scaleX: 1,
scaleY: newBounds.height / bounds.height,
initialShape: shape,
transformOrigin: [0.5, 0.5],
})
}
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 {
id: 'stretch_shapes',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
},
},
after: {
document: {
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'
export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): Command {
const { before, after } = TLDR.mutateShapes(data, ids, (shape) => {
return { style: { ...shape.style, ...changes } }
})
const { currentPageId } = data.appState
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
return { style: { ...shape.style, ...changes } }
},
currentPageId
)
return {
id: 'style_shapes',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
},
appState: {
@ -21,7 +28,7 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
after: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: after },
[currentPageId]: { shapes: after },
},
},
appState: {

View file

@ -3,34 +3,40 @@ import type { ArrowShape, Command, Data } from '~types'
import { TLDR } from '~state/tldr'
export function toggleDecoration(data: Data, ids: string[], handleId: 'start' | 'end'): Command {
const { before, after } = TLDR.mutateShapes<ArrowShape>(data, ids, (shape) => {
const decorations = shape.decorations
? {
...shape.decorations,
[handleId]: shape.decorations[handleId] ? undefined : Decoration.Arrow,
}
: {
[handleId]: Decoration.Arrow,
}
const { currentPageId } = data.appState
const { before, after } = TLDR.mutateShapes<ArrowShape>(
data,
ids,
(shape) => {
const decorations = shape.decorations
? {
...shape.decorations,
[handleId]: shape.decorations[handleId] ? undefined : Decoration.Arrow,
}
: {
[handleId]: Decoration.Arrow,
}
return {
decorations,
}
})
return {
decorations,
}
},
currentPageId
)
return {
id: 'toggle_decorations',
before: {
document: {
pages: {
[data.appState.currentPageId]: { shapes: before },
[currentPageId]: { shapes: before },
},
},
},
after: {
document: {
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'
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 { before, after } = TLDR.mutateShapes(data, TLDR.getSelectedIds(data), () => ({
[prop]: !isAllToggled,
}))
const { before, after } = TLDR.mutateShapes(
data,
TLDR.getSelectedIds(data, currentPageId),
() => ({
[prop]: !isAllToggled,
}),
currentPageId
)
return {
id: 'toggle_shapes',
before: {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: before,
},
},
@ -23,7 +29,7 @@ export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Comm
after: {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: after,
},
},

View file

@ -13,14 +13,19 @@ export function translate(data: Data, ids: string[], delta: number[]): Command {
bindings: {},
}
const change = TLDR.mutateShapes(data, ids, (shape) => ({
point: Vec.round(Vec.add(shape.point, delta)),
}))
const change = TLDR.mutateShapes(
data,
ids,
(shape) => ({
point: Vec.round(Vec.add(shape.point, delta)),
}),
data.appState.currentPageId
)
before.shapes = change.before
after.shapes = change.after
const bindingsToDelete = TLDR.getRelatedBindings(data, ids)
const bindingsToDelete = TLDR.getRelatedBindings(data, ids, data.appState.currentPageId)
bindingsToDelete.forEach((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]) {
// 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 (!shape.handles) continue

View file

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

View file

@ -31,7 +31,7 @@ export class ArrowSession implements Session {
const shapeId = pageState.selectedIds[0]
this.origin = point
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)
const initialBindingId = this.initialShape.handles[this.handleId].bindingId
@ -47,12 +47,11 @@ export class ArrowSession implements Session {
start = (data: Data) => data
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
const page = TLDR.getPage(data)
const pageState = TLDR.getPageState(data)
const page = TLDR.getPage(data, data.appState.currentPageId)
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
@ -102,7 +101,7 @@ export class ArrowSession implements Session {
if (id === initialShape.id) 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)
@ -231,12 +230,16 @@ export class ArrowSession implements Session {
complete(data: Data) {
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 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
if (initialBinding) {
@ -275,7 +278,8 @@ export class ArrowSession implements Session {
shapes: {
[initialShape.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
TLDR.getShape(data, initialShape.id, data.appState.currentPageId),
data.appState.currentPageId
),
},
bindings: afterBindings,

View file

@ -1,5 +1,6 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { TLDrawStatus } from '~types'
describe('Brush session', () => {
const tlstate = new TLDrawState()
@ -10,6 +11,7 @@ describe('Brush session', () => {
tlstate.startBrushSession([-10, -10])
tlstate.updateBrushSession([10, 10])
tlstate.completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
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> => {
const { snapshot, origin } = this
const { currentPageId } = data.appState
// Create a bounding box between the origin and the new point
const brush = Utils.getBoundsFromPoints([origin, point])
@ -29,8 +30,8 @@ export class BrushSession implements Session {
const hits = new Set<string>()
const selectedIds = new Set(snapshot.selectedIds)
const page = TLDR.getPage(data)
const pageState = TLDR.getPageState(data)
const page = TLDR.getPage(data, currentPageId)
const pageState = TLDR.getPageState(data, currentPageId)
snapshot.shapesToTest.forEach(({ id, util, selectId }) => {
if (selectedIds.has(id)) return
@ -65,7 +66,7 @@ export class BrushSession implements Session {
return {
document: {
pageStates: {
[data.appState.currentPageId]: {
[currentPageId]: {
selectedIds: Array.from(selectedIds.values()),
},
},
@ -74,10 +75,11 @@ export class BrushSession implements Session {
}
cancel(data: Data) {
const { currentPageId } = data.appState
return {
document: {
pageStates: {
[data.appState.currentPageId]: {
[currentPageId]: {
selectedIds: this.snapshot.selectedIds,
},
},
@ -86,11 +88,12 @@ export class BrushSession implements Session {
}
complete(data: Data) {
const pageState = TLDR.getPageState(data)
const { currentPageId } = data.appState
const pageState = TLDR.getPageState(data, currentPageId)
return {
document: {
pageStates: {
[data.appState.currentPageId]: {
[currentPageId]: {
selectedIds: [...pageState.selectedIds],
},
},
@ -105,9 +108,10 @@ export class BrushSession implements Session {
* brush will intersect that shape. For tests, start broad -> fine.
*/
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(
(shape) =>
!(
@ -121,7 +125,7 @@ export function getBrushSnapshot(data: Data) {
id: shape.id,
util: getShapeUtils(shape),
bounds: getShapeUtils(shape).getBounds(shape),
selectId: TLDR.getTopParentId(data, shape.id),
selectId: TLDR.getTopParentId(data, shape.id, currentPageId),
}))
return {

View file

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

View file

@ -110,18 +110,19 @@ export class DrawSession implements Session {
cancel = (data: Data) => {
const { snapshot } = this
const pageId = data.appState.currentPageId
return {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: {
[snapshot.id]: undefined,
},
},
},
pageStates: {
[data.appState.currentPageId]: {
[pageId]: {
selectedIds: [],
},
},
@ -131,19 +132,20 @@ export class DrawSession implements Session {
complete = (data: Data) => {
const { snapshot } = this
const pageId = data.appState.currentPageId
return {
id: 'create_draw',
before: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: {
[snapshot.id]: undefined,
},
},
},
pageStates: {
[data.appState.currentPageId]: {
[pageId]: {
selectedIds: [],
},
},
@ -152,9 +154,13 @@ export class DrawSession implements Session {
after: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
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
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

View file

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

View file

@ -15,10 +15,11 @@ export class HandleSession implements Session {
handleId: string
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.handleId = handleId
this.initialShape = TLDR.getShape(data, shapeId)
this.initialShape = TLDR.getShape(data, shapeId, currentPageId)
this.commandId = commandId
}
@ -26,8 +27,9 @@ export class HandleSession implements Session {
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
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
@ -54,7 +56,7 @@ export class HandleSession implements Session {
return {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: {
[shape.id]: change,
},
@ -66,11 +68,12 @@ export class HandleSession implements Session {
cancel = (data: Data) => {
const { initialShape } = this
const { currentPageId } = data.appState
return {
document: {
pages: {
[data.appState.currentPageId]: {
[currentPageId]: {
shapes: {
[initialShape.id]: initialShape,
},
@ -82,12 +85,14 @@ export class HandleSession implements Session {
complete(data: Data) {
const { initialShape } = this
const pageId = data.appState.currentPageId
return {
id: this.commandId,
before: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: {
[initialShape.id]: initialShape,
},
@ -98,11 +103,12 @@ export class HandleSession implements Session {
after: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: {
[initialShape.id]: TLDR.onSessionComplete(
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 { mockDocument } from '~test'
import { TLDrawStatus } from '~types'
describe('Brush session', () => {
const tlstate = new TLDrawState()
@ -34,6 +35,8 @@ describe('Brush session', () => {
tlstate.completeSession()
expect(tlstate.status.current).toBe(TLDrawStatus.Idle)
tlstate.undo()
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 type { Data } from '~types'
import { TLDR } from '~state/tldr'
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
const PI2 = Math.PI * 2
@ -23,8 +22,9 @@ export class RotateSession implements Session {
update = (data: Data, point: number[], isLocked = false) => {
const { commonBoundsCenter, initialShapes } = this.snapshot
const page = TLDR.getPage(data)
const pageState = TLDR.getPageState(data)
const pageId = data.appState.currentPageId
const page = TLDR.getPage(data, pageId)
const pageState = TLDR.getPageState(data, pageId)
const shapes: Record<string, TLDrawShape> = {}
@ -54,16 +54,21 @@ export class RotateSession implements Session {
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset)
shapes[id] = TLDR.mutate(data, shape, {
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
})
shapes[id] = TLDR.mutate(
data,
shape,
{
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
},
pageId
)
})
return {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes,
},
},
@ -73,6 +78,7 @@ export class RotateSession implements Session {
cancel = (data: Data) => {
const { initialShapes } = this.snapshot
const pageId = data.appState.currentPageId
const shapes: Record<string, TLDrawShape> = {}
@ -83,7 +89,7 @@ export class RotateSession implements Session {
return {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes,
},
},
@ -93,6 +99,7 @@ export class RotateSession implements Session {
complete(data: Data) {
const { hasUnlockedShapes, initialShapes } = this.snapshot
const pageId = data.appState.currentPageId
if (!hasUnlockedShapes) return data
@ -101,7 +108,7 @@ export class RotateSession implements Session {
initialShapes.forEach(({ id, shape: { 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 }
})
@ -110,7 +117,7 @@ export class RotateSession implements Session {
before: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: beforeShapes,
},
},
@ -119,7 +126,7 @@ export class RotateSession implements Session {
after: {
document: {
pages: {
[data.appState.currentPageId]: {
[pageId]: {
shapes: afterShapes,
},
},
@ -131,8 +138,9 @@ export class RotateSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getRotateSnapshot(data: Data) {
const pageState = TLDR.getPageState(data)
const initialShapes = TLDR.getSelectedBranchSnapshot(data)
const currentPageId = data.appState.currentPageId
const pageState = TLDR.getPageState(data, currentPageId)
const initialShapes = TLDR.getSelectedBranchSnapshot(data, currentPageId)
if (initialShapes.length === 0) {
throw Error('No selected shapes!')

View file

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

View file

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

View file

@ -35,7 +35,7 @@ export class TransformSingleSession implements Session {
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)
@ -95,7 +95,8 @@ export class TransformSingleSession implements Session {
beforeShapes[initialShape.id] = initialShape
afterShapes[initialShape.id] = TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
TLDR.getShape(data, initialShape.id, data.appState.currentPageId),
data.appState.currentPageId
)
return {
@ -126,7 +127,11 @@ export function getTransformSingleSnapshot(
data: Data,
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) {
throw Error('You must have one shape selected.')

View file

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

View file

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

View file

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

View file

@ -1,14 +1,5 @@
import { TLPageState, Utils, Vec } from '@tldraw/core'
import {
TLDrawShape,
TLDrawBinding,
PagePartial,
Session,
Data,
Command,
TLDrawStatus,
ShapesWithProp,
} from '~types'
import { TLDrawShape, TLDrawBinding, Session, Data, Command, TLDrawStatus } from '~types'
import { TLDR } from '~state/tldr'
export class TranslateSession implements Session {
@ -92,7 +83,8 @@ export class TranslateSession implements Session {
// Either way, move the clones
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!')
@ -129,7 +121,8 @@ export class TranslateSession implements Session {
// Move the shapes by the delta
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!')
@ -147,9 +140,9 @@ export class TranslateSession implements Session {
shapes: nextShapes,
bindings: nextBindings,
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
}
@ -183,15 +176,17 @@ export class TranslateSession implements Session {
shapes: nextShapes,
bindings: nextBindings,
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
}
}
complete(data: Data): Command {
const pageId = data.appState.currentPageId
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot
const beforeBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
@ -202,17 +197,17 @@ export class TranslateSession implements Session {
clones.forEach((shape) => {
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) => {
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) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id)
afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId)
})
bindingsToDelete.forEach((binding) => {
@ -220,7 +215,7 @@ export class TranslateSession implements Session {
for (const id of [binding.toId, binding.fromId]) {
// 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 (!shape.handles) continue
@ -279,7 +274,7 @@ export class TranslateSession implements Session {
},
pageStates: {
[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
export function getTranslateSnapshot(data: Data) {
const selectedShapes = TLDR.getSelectedShapeSnapshot(data)
const selectedShapes = TLDR.getSelectedShapeSnapshot(data, data.appState.currentPageId)
const hasUnlockedShapes = selectedShapes.length > 0
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())
.filter((id) => id !== page.id)
.map((id) => {
const shape = TLDR.getShape(data, id)
const shape = TLDR.getShape(data, id, data.appState.currentPageId)
return {
id,
children: shape.children,
@ -315,7 +310,7 @@ export function getTranslateSnapshot(data: Data) {
...shape,
id: Utils.uniqueId(),
parentId: shape.parentId,
childIndex: TLDR.getChildIndexAbove(data, shape.id),
childIndex: TLDR.getChildIndexAbove(data, shape.id, data.appState.currentPageId),
}
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(
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 {

View file

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

View file

@ -28,6 +28,22 @@ describe('TLDrawState', () => {
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', () => {

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