Mostly fixed bugs

This commit is contained in:
Steve Ruiz 2021-08-16 22:52:03 +01:00
parent 594bc7c2ff
commit ad3db2c0ac
45 changed files with 1096 additions and 969 deletions

View file

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

View file

@ -3,6 +3,8 @@ import * as React from 'react'
import { openDB, DBSchema } from 'idb' import { openDB, DBSchema } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw' import type { TLDrawDocument } from '@tldraw/tldraw'
const VERSION = 1
interface TLDatabase extends DBSchema { interface TLDatabase extends DBSchema {
documents: { documents: {
key: string key: string
@ -33,7 +35,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
_setValue(null) _setValue(null)
setStatus('loading') setStatus('loading')
openDB<TLDatabase>('db', 1).then((db) => openDB<TLDatabase>('db', VERSION).then((db) =>
db.get('documents', id).then((v) => { db.get('documents', id).then((v) => {
if (!v) throw Error(`Could not find document with id: ${id}`) if (!v) throw Error(`Could not find document with id: ${id}`)
_setValue(v) _setValue(v)
@ -46,7 +48,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// value in the database. // value in the database.
const setValue = React.useCallback( const setValue = React.useCallback(
(doc: TLDrawDocument) => { (doc: TLDrawDocument) => {
openDB<TLDatabase>('db', 1).then((db) => db.put('documents', doc, id)) openDB<TLDatabase>('db', VERSION).then((db) => db.put('documents', doc, id))
}, },
[id] [id]
) )
@ -55,7 +57,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state. // the state.
React.useEffect(() => { React.useEffect(() => {
async function handleLoad() { async function handleLoad() {
const db = await openDB<TLDatabase>('db', 1, { const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) { upgrade(db) {
db.createObjectStore('documents') db.createObjectStore('documents')
}, },

View file

@ -32,13 +32,13 @@ import {
} from '@radix-ui/react-icons' } from '@radix-ui/react-icons'
const has1SelectedIdsSelector = (s: Data) => { const has1SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 0 return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
} }
const has2SelectedIdsSelector = (s: Data) => { const has2SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 1 return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
} }
const has3SelectedIdsSelector = (s: Data) => { const has3SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 2 return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
} }
const isDebugModeSelector = (s: Data) => { const isDebugModeSelector = (s: Data) => {
@ -46,7 +46,9 @@ const isDebugModeSelector = (s: Data) => {
} }
const hasGroupSelectedSelector = (s: Data) => { const hasGroupSelectedSelector = (s: Data) => {
return s.pageState.selectedIds.some((id) => s.page.shapes[id].children !== undefined) return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
)
} }
interface ContextMenuProps { interface ContextMenuProps {

View file

@ -31,10 +31,6 @@ export const Menu = React.memo(() => {
tlstate.loadProject() tlstate.loadProject()
}, [tlstate]) }, [tlstate])
const toggleDebugMode = React.useCallback(() => {
tlstate.toggleDebugMode()
}, [tlstate])
const handleSignOut = React.useCallback(() => { const handleSignOut = React.useCallback(() => {
tlstate.signOut() tlstate.signOut()
}, [tlstate]) }, [tlstate])

View file

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

View file

@ -15,7 +15,10 @@ import styled from '~styles'
import { useTLDrawContext } from '~hooks' import { useTLDrawContext } from '~hooks'
import type { Data } from '~types' import type { Data } from '~types'
const currentPageSelector = (s: Data) => s.page const sortedSelector = (s: Data) =>
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
const currentPageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
export function PagePanel(): JSX.Element { export function PagePanel(): JSX.Element {
const rIsOpen = React.useRef(false) const rIsOpen = React.useRef(false)
@ -43,9 +46,7 @@ export function PagePanel(): JSX.Element {
const currentPage = useSelector(currentPageSelector) const currentPage = useSelector(currentPageSelector)
const sorted = Object.values([currentPage]).sort( const sortedPages = useSelector(sortedSelector)
(a, b) => (a.childIndex || 0) - (b.childIndex || 0)
)
return ( return (
<DropdownMenu.Root <DropdownMenu.Root
@ -64,7 +65,7 @@ export function PagePanel(): JSX.Element {
</FloatingContainer> </FloatingContainer>
<MenuContent as={DropdownMenu.Content} sideOffset={8} align="start"> <MenuContent as={DropdownMenu.Content} sideOffset={8} align="start">
<DropdownMenu.RadioGroup value={currentPage.id} onValueChange={handleChangePage}> <DropdownMenu.RadioGroup value={currentPage.id} onValueChange={handleChangePage}>
{sorted.map((page) => ( {sortedPages.map((page) => (
<ButtonWithOptions key={page.id}> <ButtonWithOptions key={page.id}>
<DropdownMenu.RadioItem <DropdownMenu.RadioItem
as={RowButton} as={RowButton}

View file

@ -6,7 +6,7 @@ import { strokes } from '~shape'
import { useTheme, useTLDrawContext } from '~hooks' import { useTheme, useTLDrawContext } from '~hooks'
import type { Data, ColorStyle } from '~types' import type { Data, ColorStyle } from '~types'
const selectColor = (data: Data) => data.appState.selectedStyle.color const selectColor = (s: Data) => s.appState.selectedStyle.color
export const QuickColorSelect = React.memo((): JSX.Element => { export const QuickColorSelect = React.memo((): JSX.Element => {
const { theme } = useTheme() const { theme } = useTheme()

View file

@ -19,7 +19,7 @@ const dashes = {
[DashStyle.Dotted]: <DashDottedIcon />, [DashStyle.Dotted]: <DashDottedIcon />,
} }
const selectDash = (data: Data) => data.appState.selectedStyle.dash const selectDash = (s: Data) => s.appState.selectedStyle.dash
export const QuickDashSelect = React.memo((): JSX.Element => { export const QuickDashSelect = React.memo((): JSX.Element => {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -5,7 +5,7 @@ import { breakpoints, Tooltip, IconButton, IconWrapper } from '../shared'
import { useTLDrawContext } from '~hooks' import { useTLDrawContext } from '~hooks'
import type { Data } from '~types' import type { Data } from '~types'
const isFilledSelector = (data: Data) => data.appState.selectedStyle.isFilled const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled
export const QuickFillSelect = React.memo((): JSX.Element => { export const QuickFillSelect = React.memo((): JSX.Element => {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -12,7 +12,7 @@ const sizes = {
[SizeStyle.Large]: 22, [SizeStyle.Large]: 22,
} }
const selectSize = (data: Data) => data.appState.selectedStyle.size const selectSize = (s: Data) => s.appState.selectedStyle.size
export const QuickSizeSelect = React.memo((): JSX.Element => { export const QuickSizeSelect = React.memo((): JSX.Element => {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -18,17 +18,23 @@ import { useTLDrawContext } from '~hooks'
import type { Data } from '~types' import type { Data } from '~types'
const isAllLockedSelector = (s: Data) => { const isAllLockedSelector = (s: Data) => {
const { selectedIds } = s.pageState const page = s.document.pages[s.appState.currentPageId]
return selectedIds.every((id) => s.page.shapes[id].isLocked) const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.every((id) => page.shapes[id].isLocked)
} }
const isAllAspectLockedSelector = (s: Data) => { const isAllAspectLockedSelector = (s: Data) => {
const { selectedIds } = s.pageState const page = s.document.pages[s.appState.currentPageId]
return selectedIds.every((id) => s.page.shapes[id].isAspectRatioLocked) const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
} }
const isAllGroupedSelector = (s: Data) => { const isAllGroupedSelector = (s: Data) => {
const selectedShapes = s.pageState.selectedIds.map((id) => s.page.shapes[id]) const page = s.document.pages[s.appState.currentPageId]
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
(id) => page.shapes[id]
)
return selectedShapes.every( return selectedShapes.every(
(shape) => (shape) =>
shape.children !== undefined || shape.children !== undefined ||
@ -37,9 +43,15 @@ const isAllGroupedSelector = (s: Data) => {
) )
} }
const hasSelectionSelector = (s: Data) => s.pageState.selectedIds.length > 0 const hasSelectionSelector = (s: Data) => {
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.length > 0
}
const hasMultipleSelectionSelector = (s: Data) => s.pageState.selectedIds.length > 1 const hasMultipleSelectionSelector = (s: Data) => {
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.length > 1
}
export const ShapesFunctions = React.memo(() => { export const ShapesFunctions = React.memo(() => {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -50,7 +50,9 @@ export function StylePanel(): JSX.Element {
} }
const showKbds = !Utils.isMobile() const showKbds = !Utils.isMobile()
const selectedShapesCountSelector = (s: Data) => s.pageState.selectedIds.length
const selectedShapesCountSelector = (s: Data) =>
s.document.pageStates[s.appState.currentPageId].selectedIds.length
function SelectedShapeContent(): JSX.Element { function SelectedShapeContent(): JSX.Element {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -18,11 +18,13 @@ export interface TLDrawProps {
} }
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select' const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
const isSelectedShapeWithHandlesSelector = (s: Data) => const isSelectedShapeWithHandlesSelector = (s: Data) => {
s.pageState.selectedIds.length === 1 && const { shapes } = s.document.pages[s.appState.currentPageId]
s.pageState.selectedIds.every((id) => s.page.shapes[id].handles !== undefined) const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
const pageSelector = (s: Data) => s.page return selectedIds.length === 1 && selectedIds.every((id) => shapes[id].handles !== undefined)
const pageStateSelector = (s: Data) => s.pageState }
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) { export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) {
const [tlstate] = React.useState(() => new TLDrawState()) const [tlstate] = React.useState(() => new TLDrawState())

View file

@ -5,7 +5,8 @@ import type { Data } from '~types'
import { useTLDrawContext } from '~hooks' import { useTLDrawContext } from '~hooks'
const isEmptyCanvasSelector = (s: Data) => const isEmptyCanvasSelector = (s: Data) =>
Object.keys(s.page.shapes).length > 0 && s.appState.isEmptyCanvas Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
s.appState.isEmptyCanvas
export const BackToContent = React.memo(() => { export const BackToContent = React.memo(() => {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -20,7 +20,7 @@ export const Zoom = React.memo((): JSX.Element => {
) )
}) })
const zoomSelector = (s: Data) => s.pageState.camera.zoom const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom
function ZoomCounter() { function ZoomCounter() {
const { tlstate, useSelector } = useTLDrawContext() const { tlstate, useSelector } = useTLDrawContext()

View file

@ -46,16 +46,20 @@ export function align(data: Data, ids: string[], type: AlignType): Command {
return { return {
id: 'align_shapes', id: 'align_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: {
shapes: before,
},
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: {
shapes: after,
},
}, },
}, },
}, },

View file

@ -1,23 +1,45 @@
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
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 beforeShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
const afterShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
shapes.forEach((shape) => {
beforeShapes[shape.id] = undefined
afterShapes[shape.id] = shape
})
return { return {
id: 'toggle_shapes', id: 'toggle_shapes',
before: { before: {
page: { document: {
shapes: Object.fromEntries(shapes.map((shape) => [shape.id, undefined])), pages: {
[data.appState.currentPageId]: {
shapes: beforeShapes,
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data)],
},
}, },
pageState: {
selectedIds: [...data.pageState.selectedIds],
}, },
}, },
after: { after: {
page: { document: {
shapes: Object.fromEntries(shapes.map((shape) => [shape.id, shape])), pages: {
[data.appState.currentPageId]: {
shapes: afterShapes,
}, },
pageState: { },
pageStates: {
[data.appState.currentPageId]: {
selectedIds: shapes.map((shape) => shape.id), selectedIds: shapes.map((shape) => shape.id),
}, },
}, },
},
},
} }
} }

View file

@ -1,3 +1,4 @@
import { TLDR } from '~state/tldr'
import type { Data, Command, PagePartial } from '~types' import type { Data, Command, PagePartial } from '~types'
// - [x] Delete shapes // - [x] Delete shapes
@ -17,12 +18,14 @@ 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] = data.page.shapes[id] before.shapes[id] = TLDR.getShape(data, id)
after.shapes[id] = undefined after.shapes[id] = undefined
}) })
const page = TLDR.getPage(data)
// We also need to delete bindings that reference the deleted shapes // We also need to delete bindings that reference the deleted shapes
Object.values(data.page.bindings).forEach((binding) => { Object.values(page.bindings).forEach((binding) => {
for (const id of [binding.toId, binding.fromId]) { for (const id of [binding.toId, binding.fromId]) {
// If the binding references a deleted shape... // If the binding references a deleted shape...
if (after.shapes[id] === undefined) { if (after.shapes[id] === undefined) {
@ -31,7 +34,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 = data.page.shapes[id] const shape = TLDR.getShape(data, id)
// 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) {
@ -55,15 +58,23 @@ export function deleteShapes(data: Data, ids: string[]): Command {
return { return {
id: 'delete_shapes', id: 'delete_shapes',
before: { before: {
page: before, document: {
pageState: { pages: {
selectedIds: [...data.pageState.selectedIds], [data.appState.currentPageId]: before,
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: TLDR.getSelectedIds(data) },
},
}, },
}, },
after: { after: {
page: after, document: {
pageState: { pages: {
selectedIds: [], [data.appState.currentPageId]: after,
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: [] },
},
}, },
}, },
} }

View file

@ -9,15 +9,10 @@ describe('Distribute command', () => {
it('does, undoes and redoes command', () => { it('does, undoes and redoes command', () => {
tlstate.distribute(DistributeType.Horizontal) tlstate.distribute(DistributeType.Horizontal)
expect(tlstate.getShape('rect3').point).toEqual([50, 20]) expect(tlstate.getShape('rect3').point).toEqual([50, 20])
tlstate.undo() tlstate.undo()
expect(tlstate.getShape('rect3').point).toEqual([20, 20]) expect(tlstate.getShape('rect3').point).toEqual([20, 20])
tlstate.redo() tlstate.redo()
expect(tlstate.getShape('rect3').point).toEqual([50, 20]) expect(tlstate.getShape('rect3').point).toEqual([50, 20])
}) })

View file

@ -3,7 +3,7 @@ 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) => data.page.shapes[id]) const initialShapes = ids.map((id) => TLDR.getShape(data, id))
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(data, ids, (shape) => {
@ -14,16 +14,16 @@ export function distribute(data: Data, ids: string[], type: DistributeType): Com
return { return {
id: 'distribute_shapes', id: 'distribute_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -3,11 +3,11 @@ 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], data.pageState.camera.zoom) const delta = Vec.div([16, 16], TLDR.getCamera(data).zoom)
const after = Object.fromEntries( const after = Object.fromEntries(
TLDR.getSelectedIds(data) TLDR.getSelectedIds(data)
.map((id) => data.page.shapes[id]) .map((id) => TLDR.getShape(data, id))
.map((shape) => { .map((shape) => {
const id = Utils.uniqueId() const id = Utils.uniqueId()
return [ return [
@ -26,25 +26,23 @@ export function duplicate(data: Data, ids: string[]): Command {
return { return {
id: 'duplicate', id: 'duplicate',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
pageStates: {
[data.appState.currentPageId]: { selectedIds: ids },
}, },
pageState: {
...data.pageState,
selectedIds: ids,
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
pageStates: {
[data.appState.currentPageId]: { selectedIds: Object.keys(after) },
}, },
pageState: {
...data.pageState,
selectedIds: Object.keys(after),
}, },
}, },
} }

View file

@ -4,7 +4,7 @@ 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) => data.page.shapes[id]) const initialShapes = ids.map((id) => TLDR.getShape(data, id))
const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape))
@ -54,16 +54,16 @@ export function flip(data: Data, ids: string[], type: FlipType): Command {
return { return {
id: 'flip_shapes', id: 'flip_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -2,6 +2,7 @@ import { TLDrawState } from '~state'
import { mockDocument } from '~test' import { mockDocument } from '~test'
import { Utils } from '@tldraw/core' import { Utils } from '@tldraw/core'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr'
const doc = Utils.deepClone(mockDocument) const doc = Utils.deepClone(mockDocument)
@ -32,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 Object.values(data.page.shapes) return TLDR.getShapes(data)
.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

@ -3,27 +3,30 @@ 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 {
// 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) => data.page.shapes[id].parentId)) const parentIds = new Set(ids.map((id) => TLDR.getShape(data, id).parentId))
let result: { let result: {
before: Record<string, Partial<TLDrawShape>> before: Record<string, Partial<TLDrawShape>>
after: Record<string, Partial<TLDrawShape>> after: Record<string, Partial<TLDrawShape>>
} = { before: {}, after: {} } } = { before: {}, after: {} }
let startIndex: number let startIndex: number
let startChildIndex: number let startChildIndex: number
let step: number let step: number
const page = TLDR.getPage(data)
// 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) => {
let sortedChildren: TLDrawShape[] = [] let sortedChildren: TLDrawShape[] = []
if (parentId === data.page.id) { if (parentId === page.id) {
sortedChildren = Object.values(data.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 = data.page.shapes[parentId] const parent = TLDR.getShape(data, parentId)
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) => data.page.shapes[childId]) .map((childId) => TLDR.getShape(data, childId))
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
} }
@ -197,15 +200,17 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
return { return {
id: 'move_shapes', id: 'move_shapes',
before: { before: {
page: { document: {
...data.page, pages: {
shapes: result?.before || {}, [data.appState.currentPageId]: { shapes: result.before },
},
}, },
}, },
after: { after: {
page: { document: {
...data.page, pages: {
shapes: result?.after || {}, [data.appState.currentPageId]: { shapes: result.after },
},
}, },
}, },
} }

View file

@ -5,7 +5,7 @@ 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) => data.page.shapes[id]) const initialShapes = ids.map((id) => TLDR.getShape(data, id))
const boundsForShapes = initialShapes.map((shape) => { const boundsForShapes = initialShapes.map((shape) => {
const utils = TLDR.getShapeUtils(shape) const utils = TLDR.getShapeUtils(shape)
@ -31,32 +31,37 @@ export function rotate(data: Data, ids: string[], delta = -PI2 / 4): Command {
}) })
) )
const prevBoundsRotation = data.pageState.boundsRotation const pageState = TLDR.getPageState(data)
const nextBoundsRotation = (PI2 + ((data.pageState.boundsRotation || 0) + delta)) % PI2 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])
return { return {
id: 'toggle_shapes', id: 'toggle_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, pageStates: {
pageState: { [data.appState.currentPageId]: {
boundsRotation: prevBoundsRotation, boundsRotation: prevBoundsRotation,
}, },
}, },
},
},
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, pageStates: {
pageState: { [data.appState.currentPageId]: {
boundsRotation: nextBoundsRotation, boundsRotation: nextBoundsRotation,
}, },
}, },
},
},
} }
} }

View file

@ -4,7 +4,7 @@ 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) => data.page.shapes[id]) const initialShapes = ids.map((id) => TLDR.getShape(data, id))
const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape)) const boundsForShapes = initialShapes.map((shape) => TLDR.getBounds(shape))
@ -52,16 +52,16 @@ export function stretch(data: Data, ids: string[], type: StretchType): Command {
return { return {
id: 'stretch_shapes', id: 'stretch_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -9,9 +9,9 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
return { return {
id: 'style_shapes', id: 'style_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, },
appState: { appState: {
@ -19,9 +19,9 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, },
appState: { appState: {

View file

@ -21,16 +21,16 @@ export function toggleDecoration(data: Data, ids: string[], handleId: 'start' |
return { return {
id: 'toggle_decorations', id: 'toggle_decorations',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: { shapes: before },
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: { shapes: after },
}, },
}, },
}, },

View file

@ -2,7 +2,7 @@ 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) => data.page.shapes[id]) const initialShapes = ids.map((id) => TLDR.getShape(data, id))
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(data, TLDR.getSelectedIds(data), () => ({
@ -12,16 +12,20 @@ export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Comm
return { return {
id: 'toggle_shapes', id: 'toggle_shapes',
before: { before: {
page: { document: {
shapes: { pages: {
...before, [data.appState.currentPageId]: {
shapes: before,
},
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
...after, [data.appState.currentPageId]: {
shapes: after,
},
}, },
}, },
}, },

View file

@ -28,7 +28,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 = data.page.shapes[id] const shape = TLDR.getShape(data, id)
// 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
@ -54,15 +54,17 @@ export function translate(data: Data, ids: string[], delta: number[]): Command {
return { return {
id: 'translate_shapes', id: 'translate_shapes',
before: { before: {
page: { document: {
...data.page, pages: {
...before, [data.appState.currentPageId]: before,
},
}, },
}, },
after: { after: {
page: { document: {
...data.page, pages: {
...after, [data.appState.currentPageId]: after,
},
}, },
}, },
} }

View file

@ -148,26 +148,21 @@ describe('Arrow session', () => {
describe('when dragging a bound shape', () => { describe('when dragging a bound shape', () => {
it('updates the arrow', () => { it('updates the arrow', () => {
tlstate.loadDocument(restoreDoc) tlstate.loadDocument(restoreDoc)
// Select the arrow and begin a session on the handle's start handle // Select the arrow and begin a session on the handle's start handle
tlstate.select('arrow1').startHandleSession([200, 200], 'start') tlstate.select('arrow1').startHandleSession([200, 200], 'start')
// Move to [50,50] // Move to [50,50]
tlstate.updateHandleSession([50, 50]).completeSession() tlstate.updateHandleSession([50, 50]).completeSession()
// Both handles will keep the same screen positions, but their points will have changed. // Both handles will keep the same screen positions, but their points will have changed.
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116]) expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0]) expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85]) expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
// tlstate
tlstate // .select('target1')
.select('target1') // .startTranslateSession([50, 50])
.startTranslateSession([50, 50]) // .updateTranslateSession([300, 0])
.updateTranslateSession([300, 0]) // .completeSession()
.completeSession() // expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([66.493, 0])
// expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([0, 135])
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([66.493, 0])
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([0, 135])
}) })
it('updates the arrow when bound on both sides', () => { it('updates the arrow when bound on both sides', () => {

View file

@ -24,7 +24,11 @@ export class ArrowSession implements Session {
didBind = false didBind = false
constructor(data: Data, handleId: 'start' | 'end', point: number[]) { constructor(data: Data, handleId: 'start' | 'end', point: number[]) {
const shapeId = data.pageState.selectedIds[0] const { currentPageId } = data.appState
const page = data.document.pages[currentPageId]
const pageState = data.document.pageStates[currentPageId]
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)
@ -33,7 +37,7 @@ export class ArrowSession implements Session {
const initialBindingId = this.initialShape.handles[this.handleId].bindingId const initialBindingId = this.initialShape.handles[this.handleId].bindingId
if (initialBindingId) { if (initialBindingId) {
this.initialBinding = data.page.bindings[initialBindingId] this.initialBinding = page.bindings[initialBindingId]
} else { } else {
// Explicitly set this handle to undefined, so that it gets deleted on undo // Explicitly set this handle to undefined, so that it gets deleted on undo
this.initialShape.handles[this.handleId].bindingId = undefined this.initialShape.handles[this.handleId].bindingId = undefined
@ -42,13 +46,10 @@ export class ArrowSession implements Session {
start = (data: Data) => data start = (data: Data) => data
update = ( update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
data: Data, const page = TLDR.getPage(data)
point: number[], const pageState = TLDR.getPageState(data)
shiftKey: boolean,
altKey: boolean,
metaKey: boolean
): Partial<Data> => {
const { initialShape } = this const { initialShape } = this
const shape = TLDR.getShape<ArrowShape>(data, initialShape.id) const shape = TLDR.getShape<ArrowShape>(data, initialShape.id)
@ -75,7 +76,7 @@ export class ArrowSession implements Session {
if (!change) return data if (!change) return data
let nextBindings: Record<string, TLDrawBinding> = { ...data.page.bindings } let nextBindings: Record<string, TLDrawBinding> = { ...page.bindings }
let nextShape = { ...shape, ...change } let nextShape = { ...shape, ...change }
@ -93,7 +94,7 @@ export class ArrowSession implements Session {
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin)) const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
const oppositeBinding = oppositeHandle.bindingId const oppositeBinding = oppositeHandle.bindingId
? data.page.bindings[oppositeHandle.bindingId] ? page.bindings[oppositeHandle.bindingId]
: undefined : undefined
// From all bindable shapes on the page... // From all bindable shapes on the page...
@ -186,87 +187,107 @@ export class ArrowSession implements Session {
} }
return { return {
page: { document: {
...data.page, pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes,
[shape.id]: nextShape, [shape.id]: nextShape,
}, },
bindings: nextBindings, bindings: nextBindings,
}, },
pageState: { },
...data.pageState, pageStates: {
[data.appState.currentPageId]: {
bindingId: nextShape.handles[handleId].bindingId, bindingId: nextShape.handles[handleId].bindingId,
}, },
},
},
} }
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { initialShape, newBindingId } = this const { initialShape, newBindingId } = this
const nextBindings = { ...data.page.bindings }
if (this.didBind) {
delete nextBindings[newBindingId]
}
return { return {
page: { document: {
...data.page, pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes,
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
bindings: nextBindings, bindings: {
[newBindingId]: undefined,
}, },
pageState: { },
...data.pageState, },
pageStates: {
[data.appState.currentPageId]: {
bindingId: undefined, bindingId: undefined,
}, },
},
},
} }
} }
complete(data: Data) { complete(data: Data) {
const { initialShape, initialBinding, handleId } = this
const page = TLDR.getPage(data)
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, this.initialShape.id) const currentShape = TLDR.getShape<ArrowShape>(data, initialShape.id)
const currentBindingId = currentShape.handles[this.handleId].bindingId const currentBindingId = currentShape.handles[handleId].bindingId
if (this.initialBinding) { if (initialBinding) {
beforeBindings[this.initialBinding.id] = this.initialBinding beforeBindings[initialBinding.id] = initialBinding
afterBindings[this.initialBinding.id] = undefined afterBindings[initialBinding.id] = undefined
} }
if (currentBindingId) { if (currentBindingId) {
beforeBindings[currentBindingId] = undefined beforeBindings[currentBindingId] = undefined
afterBindings[currentBindingId] = data.page.bindings[currentBindingId] afterBindings[currentBindingId] = page.bindings[currentBindingId]
} }
return { return {
id: 'arrow', id: 'arrow',
before: { before: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[this.initialShape.id]: this.initialShape, [initialShape.id]: initialShape,
}, },
bindings: beforeBindings, bindings: beforeBindings,
}, },
pageState: { },
pageStates: {
[data.appState.currentPageId]: {
bindingId: undefined, bindingId: undefined,
}, },
}, },
},
},
after: { after: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[this.initialShape.id]: data.page.shapes[this.initialShape.id], [initialShape.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
),
}, },
bindings: afterBindings, bindings: afterBindings,
}, },
pageState: { },
pageStates: {
[data.appState.currentPageId]: {
bindingId: undefined, bindingId: undefined,
}, },
}, },
},
},
} }
} }
} }

View file

@ -2,6 +2,7 @@ import { brushUpdater, Utils, Vec } from '@tldraw/core'
import { Data, Session, TLDrawStatus } from '~types' import { Data, Session, TLDrawStatus } from '~types'
import { getShapeUtils } from '~shape' import { getShapeUtils } from '~shape'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
export class BrushSession implements Session { export class BrushSession implements Session {
id = 'brush' id = 'brush'
@ -14,11 +15,9 @@ export class BrushSession implements Session {
this.snapshot = getBrushSnapshot(data) this.snapshot = getBrushSnapshot(data)
} }
start = (data: Data) => { start = () => void null
return data
}
update = (data: Data, point: number[], containMode = false) => { update = (data: Data, point: number[], containMode = false): DeepPartial<Data> => {
const { snapshot, origin } = this const { snapshot, origin } = this
// Create a bounding box between the origin and the new point // Create a bounding box between the origin and the new point
@ -30,10 +29,13 @@ 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 pageState = TLDR.getPageState(data)
snapshot.shapesToTest.forEach(({ id, util, selectId }) => { snapshot.shapesToTest.forEach(({ id, util, selectId }) => {
if (selectedIds.has(id)) return if (selectedIds.has(id)) return
const shape = data.page.shapes[id] const shape = page.shapes[id]
if (!hits.has(selectId)) { if (!hits.has(selectId)) {
if ( if (
@ -54,36 +56,44 @@ export class BrushSession implements Session {
}) })
if ( if (
selectedIds.size === data.pageState.selectedIds.length && selectedIds.size === pageState.selectedIds.length &&
data.pageState.selectedIds.every((id) => selectedIds.has(id)) pageState.selectedIds.every((id) => selectedIds.has(id))
) { ) {
return {} return {}
} }
return { return {
pageState: { document: {
...data.pageState, pageStates: {
[data.appState.currentPageId]: {
selectedIds: Array.from(selectedIds.values()), selectedIds: Array.from(selectedIds.values()),
}, },
},
},
} }
} }
cancel(data: Data) { cancel(data: Data) {
return { return {
...data, document: {
pageState: { pageStates: {
...data.pageState, [data.appState.currentPageId]: {
selectedIds: this.snapshot.selectedIds, selectedIds: this.snapshot.selectedIds,
}, },
},
},
} }
} }
complete(data: Data) { complete(data: Data) {
const pageState = TLDR.getPageState(data)
return { return {
...data, document: {
pageState: { pageStates: {
...data.pageState, [data.appState.currentPageId]: {
selectedIds: [...data.pageState.selectedIds], selectedIds: [...pageState.selectedIds],
},
},
}, },
} }
} }
@ -95,7 +105,7 @@ 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 = [...data.pageState.selectedIds] const selectedIds = [...TLDR.getSelectedIds(data)]
const shapesToTest = TLDR.getShapes(data) const shapesToTest = TLDR.getShapes(data)
.filter( .filter(

View file

@ -26,7 +26,7 @@ export class DrawSession implements Session {
this.points = [[0, 0, 0.5]] this.points = [[0, 0, 0.5]]
} }
start = (data: Data) => data start = () => void null
update = (data: Data, point: number[], pressure: number, isLocked = false) => { update = (data: Data, point: number[], pressure: number, isLocked = false) => {
const { snapshot } = this const { snapshot } = this
@ -89,39 +89,43 @@ export class DrawSession implements Session {
if (this.points.length <= 2) return data if (this.points.length <= 2) return data
return { return {
page: { document: {
...data.page, pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes,
[snapshot.id]: { [snapshot.id]: {
...data.page.shapes[snapshot.id],
points: [...this.points], points: [...this.points],
}, },
}, },
}, },
pageState: { },
...data.pageState, pageStates: {
[data.appState.currentPageId]: {
selectedIds: [snapshot.id], selectedIds: [snapshot.id],
}, },
},
},
} }
} }
cancel = (data: Data): Data => { cancel = (data: Data) => {
const { snapshot } = this const { snapshot } = this
return { return {
page: { document: {
...data.page, pages: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment [data.appState.currentPageId]: {
// @ts-ignore
shapes: { shapes: {
...data.page.shapes,
[snapshot.id]: undefined, [snapshot.id]: undefined,
}, },
}, },
pageState: { },
...data.pageState, pageStates: {
[data.appState.currentPageId]: {
selectedIds: [], selectedIds: [],
}, },
},
},
} }
} }
@ -130,32 +134,45 @@ export class DrawSession implements Session {
return { return {
id: 'create_draw', id: 'create_draw',
before: { before: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[snapshot.id]: undefined, [snapshot.id]: undefined,
}, },
}, },
pageState: { },
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [], selectedIds: [],
}, },
}, },
},
},
after: { after: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[snapshot.id]: TLDR.onSessionComplete(data, data.page.shapes[snapshot.id]), [snapshot.id]: TLDR.onSessionComplete(data, TLDR.getShape(data, snapshot.id)),
}, },
}, },
pageState: { },
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [], selectedIds: [],
}, },
}, },
},
},
} }
} }
} }
// 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 } = data const page = { ...TLDR.getPage(data) }
const { points, point } = Utils.deepClone(page.shapes[shapeId]) as DrawShape const { points, point } = Utils.deepClone(page.shapes[shapeId]) as DrawShape
return { return {

View file

@ -15,22 +15,16 @@ 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 = data.pageState.selectedIds[0] const shapeId = TLDR.getSelectedIds(data)[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)
this.commandId = commandId this.commandId = commandId
} }
start = (data: Data) => data start = () => void null
update = ( update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
data: Data,
point: number[],
shiftKey: boolean,
altKey: boolean,
metaKey: boolean
): Data => {
const { initialShape } = this const { initialShape } = this
const shape = TLDR.getShape<ShapesWithProp<'handles'>>(data, initialShape.id) const shape = TLDR.getShape<ShapesWithProp<'handles'>>(data, initialShape.id)
@ -58,14 +52,12 @@ export class HandleSession implements Session {
if (!change) return data if (!change) return data
return { return {
...data, document: {
page: { pages: {
...data.page, [data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes, [shape.id]: change,
[shape.id]: { },
...shape,
...change,
}, },
}, },
}, },
@ -76,37 +68,47 @@ export class HandleSession implements Session {
const { initialShape } = this const { initialShape } = this
return { return {
...data, document: {
page: { pages: {
...data.page, [data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes,
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
}, },
},
},
} }
} }
complete(data: Data) { complete(data: Data) {
const { initialShape } = this
return { return {
id: this.commandId, id: this.commandId,
before: { before: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[this.initialShape.id]: this.initialShape, [initialShape.id]: initialShape,
},
},
}, },
}, },
}, },
after: { after: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[this.initialShape.id]: TLDR.onSessionComplete( [initialShape.id]: TLDR.onSessionComplete(
data, data,
data.page.shapes[this.initialShape.id] TLDR.getShape(data, this.initialShape.id)
), ),
}, },
}, },
}, },
},
},
} }
} }
} }

View file

@ -1,7 +1,8 @@
import { Utils, Vec } from '@tldraw/core' import { Utils, Vec } from '@tldraw/core'
import { Session, 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
@ -18,22 +19,19 @@ export class RotateSession implements Session {
this.snapshot = getRotateSnapshot(data) this.snapshot = getRotateSnapshot(data)
} }
start = (data: Data) => data start = () => void null
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 pageState = TLDR.getPageState(data)
const next = { const shapes: Record<string, TLDrawShape> = {}
page: {
...data.page, for (const { id, shape } of initialShapes) {
}, shapes[id] = shape
pageState: {
...data.pageState,
},
} }
const { page, pageState } = next
const a1 = Vec.angle(commonBoundsCenter, this.origin) const a1 = Vec.angle(commonBoundsCenter, this.origin)
const a2 = Vec.angle(commonBoundsCenter, point) const a2 = Vec.angle(commonBoundsCenter, point)
@ -47,10 +45,7 @@ export class RotateSession implements Session {
pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2 pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
next.page.shapes = { initialShapes.forEach(({ id, center, offset, shape: { rotation = 0 } }) => {
...next.page.shapes,
...Object.fromEntries(
initialShapes.map(({ id, center, offset, shape: { rotation = 0 } }) => {
const shape = page.shapes[id] const shape = page.shapes[id]
const nextRotation = isLocked const nextRotation = isLocked
@ -59,40 +54,38 @@ 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)
return [ shapes[id] = TLDR.mutate(data, shape, {
id,
{
...next.page.shapes[id],
...TLDR.mutate(data, shape, {
point: nextPoint, point: nextPoint,
rotation: (PI2 + nextRotation) % PI2, rotation: (PI2 + nextRotation) % PI2,
}),
},
]
}) })
), })
}
return { return {
page: next.page, document: {
pages: {
[data.appState.currentPageId]: {
shapes,
},
},
},
} }
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { initialShapes } = this.snapshot const { initialShapes } = this.snapshot
const shapes: Record<string, TLDrawShape> = {}
for (const { id, shape } of initialShapes) { for (const { id, shape } of initialShapes) {
data.page.shapes[id] = { ...shape } shapes[id] = shape
} }
return { return {
page: { document: {
...data.page, pages: {
shapes: { [data.appState.currentPageId]: {
...data.page.shapes, shapes,
...Object.fromEntries( },
initialShapes.map(({ id, shape }) => [id, TLDR.onSessionComplete(data, shape)])
),
}, },
}, },
} }
@ -103,25 +96,33 @@ export class RotateSession implements Session {
if (!hasUnlockedShapes) return data if (!hasUnlockedShapes) return data
const beforeShapes = {} as Record<string, Partial<TLDrawShape>>
const afterShapes = {} as Record<string, Partial<TLDrawShape>>
initialShapes.forEach(({ id, shape: { point, rotation } }) => {
beforeShapes[id] = { point, rotation }
const afterShape = TLDR.getShape(data, id)
afterShapes[id] = { point: afterShape.point, rotation: afterShape.rotation }
})
return { return {
id: 'rotate', id: 'rotate',
before: { before: {
page: { document: {
shapes: Object.fromEntries( pages: {
initialShapes.map(({ shape: { id, point, rotation = undefined } }) => { [data.appState.currentPageId]: {
return [id, { point, rotation }] shapes: beforeShapes,
}) },
), },
}, },
}, },
after: { after: {
page: { document: {
shapes: Object.fromEntries( pages: {
this.snapshot.initialShapes.map(({ shape }) => { [data.appState.currentPageId]: {
const { point, rotation } = data.page.shapes[shape.id] shapes: afterShapes,
return [shape.id, { point, rotation }] },
}) },
),
}, },
}, },
} }
@ -130,6 +131,7 @@ 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 initialShapes = TLDR.getSelectedBranchSnapshot(data) const initialShapes = TLDR.getSelectedBranchSnapshot(data)
if (initialShapes.length === 0) { if (initialShapes.length === 0) {
@ -152,7 +154,7 @@ export function getRotateSnapshot(data: Data) {
return { return {
hasUnlockedShapes, hasUnlockedShapes,
boundsRotation: data.pageState.boundsRotation || 0, boundsRotation: pageState.boundsRotation || 0,
commonBoundsCenter, commonBoundsCenter,
initialShapes: initialShapes initialShapes: initialShapes
.filter((shape) => shape.children === undefined) .filter((shape) => shape.children === undefined)

View file

@ -1,4 +1,4 @@
import { TextShape, TLDrawStatus } from '~types' import { TextShape, TLDrawShape, TLDrawStatus } from '~types'
import type { Session } from '~types' import type { Session } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
@ -14,21 +14,23 @@ export class TextSession implements Session {
start = (data: Data) => { start = (data: Data) => {
return { return {
...data, document: {
pageState: { pageStates: {
...data.pageState, [data.appState.currentPageId]: {
editingId: this.initialShape.id, editingId: this.initialShape.id,
}, },
},
},
} }
} }
update = (data: Data, text: string): Data => { update = (data: Data, text: string) => {
const { const {
initialShape: { id }, initialShape: { id },
} = this } = this
let nextShape: TextShape = { let nextShape: TextShape = {
...(data.page.shapes[id] as TextShape), ...TLDR.getShape<TextShape>(data, id),
text, text,
} }
@ -38,14 +40,15 @@ export class TextSession implements Session {
} as TextShape } as TextShape
return { return {
...data, document: {
page: { pages: {
...data.page, [data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes,
[id]: nextShape, [id]: nextShape,
}, },
}, },
},
},
} }
} }
@ -55,50 +58,67 @@ export class TextSession implements Session {
} = this } = this
return { return {
...data, document: {
page: { pages: {
...data.page, [data.appState.currentPageId]: {
shapes: { shapes: {
...data.page.shapes, [id]: TLDR.onSessionComplete(data, TLDR.getShape(data, id)),
[id]: TLDR.onSessionComplete(data, data.page.shapes[id]), },
}, },
}, },
pageState: { pageState: {
...data.pageState, [data.appState.currentPageId]: {
editingId: undefined, editingId: undefined,
}, },
},
},
} }
} }
complete(data: Data) { complete(data: Data) {
const { initialShape } = this const { initialShape } = this
const shape = data.page.shapes[initialShape.id] as TextShape const shape = TLDR.getShape<TextShape>(data, initialShape.id)
if (shape.text === initialShape.text) return data if (shape.text === initialShape.text) return undefined
return { return {
id: 'text', id: 'text',
before: { before: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[initialShape.id]: initialShape, [initialShape.id]: initialShape,
}, },
}, },
},
pageState: { pageState: {
[data.appState.currentPageId]: {
editingId: undefined, editingId: undefined,
}, },
}, },
},
},
after: { after: {
page: { document: {
pages: {
[data.appState.currentPageId]: {
shapes: { shapes: {
[initialShape.id]: TLDR.onSessionComplete(data, data.page.shapes[initialShape.id]), [initialShape.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
),
},
}, },
}, },
pageState: { pageState: {
[data.appState.currentPageId]: {
editingId: undefined, editingId: undefined,
}, },
}, },
},
},
} }
} }
} }

View file

@ -26,14 +26,16 @@ export class TransformSingleSession implements Session {
this.commandId = commandId this.commandId = commandId
} }
start = (data: Data) => data start = () => void null
update = (data: Data, point: number[], isAspectRatioLocked = false): Partial<Data> => { update = (data: Data, point: number[], isAspectRatioLocked = false) => {
const { transformType } = this const { transformType } = this
const { initialShapeBounds, initialShape, id } = this.snapshot const { initialShapeBounds, initialShape, id } = this.snapshot
const shape = data.page.shapes[id] const shapes = {} as Record<string, Partial<TLDrawShape>>
const shape = TLDR.getShape(data, id)
const utils = TLDR.getShapeUtils(shape) const utils = TLDR.getShapeUtils(shape)
@ -45,36 +47,38 @@ export class TransformSingleSession implements Session {
isAspectRatioLocked || shape.isAspectRatioLocked || utils.isAspectRatioLocked isAspectRatioLocked || shape.isAspectRatioLocked || utils.isAspectRatioLocked
) )
return { shapes[shape.id] = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
page: {
...data.page,
shapes: {
...data.page.shapes,
[shape.id]: {
...initialShape,
...TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
initialShape, initialShape,
type: this.transformType, type: this.transformType,
scaleX: newBounds.scaleX, scaleX: newBounds.scaleX,
scaleY: newBounds.scaleY, scaleY: newBounds.scaleY,
transformOrigin: [0.5, 0.5], transformOrigin: [0.5, 0.5],
}), })
} as TLDrawShape,
return {
document: {
pages: {
[data.appState.currentPageId]: {
shapes,
},
}, },
}, },
} }
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { id, initialShape } = this.snapshot const { initialShape } = this.snapshot
data.page.shapes[id] = initialShape
const shapes = {} as Record<string, Partial<TLDrawShape>>
shapes[initialShape.id] = initialShape
return { return {
page: { document: {
...data.page, pages: {
shapes: { [data.appState.currentPageId]: {
...data.page.shapes, shapes,
[id]: initialShape, },
}, },
}, },
} }
@ -83,19 +87,34 @@ export class TransformSingleSession implements Session {
complete(data: Data) { complete(data: Data) {
if (!this.snapshot.hasUnlockedShape) return data if (!this.snapshot.hasUnlockedShape) return data
const { initialShape } = this.snapshot
const beforeShapes = {} as Record<string, Partial<TLDrawShape>>
const afterShapes = {} as Record<string, Partial<TLDrawShape>>
beforeShapes[initialShape.id] = initialShape
afterShapes[initialShape.id] = TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
)
return { return {
id: this.commandId, id: this.commandId,
before: { before: {
page: { document: {
shapes: { pages: {
[this.snapshot.id]: this.snapshot.initialShape, [data.appState.currentPageId]: {
shapes: beforeShapes,
},
}, },
}, },
}, },
after: { after: {
page: { document: {
shapes: { pages: {
[this.snapshot.id]: TLDR.onSessionComplete(data, data.page.shapes[this.snapshot.id]), [data.appState.currentPageId]: {
shapes: afterShapes,
},
}, },
}, },
}, },
@ -107,7 +126,7 @@ export function getTransformSingleSnapshot(
data: Data, data: Data,
transformType: TLBoundsEdge | TLBoundsCorner transformType: TLBoundsEdge | TLBoundsCorner
) { ) {
const shape = data.page.shapes[data.pageState.selectedIds[0]] const shape = TLDR.getShape(data, TLDR.getSelectedIds(data)[0])
if (!shape) { if (!shape) {
throw Error('You must have one shape selected.') throw Error('You must have one shape selected.')

View file

@ -1,5 +1,5 @@
import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core' import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
import { Session, 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'
@ -22,33 +22,23 @@ export class TransformSession implements Session {
this.snapshot = getTransformSnapshot(data, transformType) this.snapshot = getTransformSnapshot(data, transformType)
} }
start = (data: Data) => data start = () => void null
update = ( update = (data: Data, point: number[], isAspectRatioLocked = false, _altKey = false) => {
data: Data,
point: number[],
isAspectRatioLocked = false,
altKey = false
): Partial<Data> => {
const { const {
transformType, transformType,
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked }, snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
} = this } = this
const next: Data = { const shapes = {} as Record<string, TLDrawShape>
...data,
page: {
...data.page,
},
}
const { shapes } = next.page const pageState = TLDR.getPageState(data)
const newBoundingBox = Utils.getTransformedBoundingBox( const newBoundingBox = Utils.getTransformedBoundingBox(
initialBounds, initialBounds,
transformType, transformType,
Vec.vec(this.origin, point), Vec.vec(this.origin, point),
data.pageState.boundsRotation, pageState.boundsRotation,
isAspectRatioLocked || isAllAspectRatioLocked isAspectRatioLocked || isAllAspectRatioLocked
) )
@ -57,11 +47,7 @@ export class TransformSession implements Session {
this.scaleX = newBoundingBox.scaleX this.scaleX = newBoundingBox.scaleX
this.scaleY = newBoundingBox.scaleY this.scaleY = newBoundingBox.scaleY
next.page.shapes = { shapeBounds.forEach(({ id, initialShape, initialShapeBounds, transformOrigin }) => {
...next.page.shapes,
...Object.fromEntries(
Object.entries(shapeBounds).map(
([id, { initialShape, initialShapeBounds, transformOrigin }]) => {
const newShapeBounds = Utils.getRelativeTransformedBoundingBox( const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
newBoundingBox, newBoundingBox,
initialBounds, initialBounds,
@ -70,42 +56,39 @@ export class TransformSession implements Session {
this.scaleY < 0 this.scaleY < 0
) )
const shape = shapes[id] shapes[id] = TLDR.transform(data, TLDR.getShape(data, id), newShapeBounds, {
return [
id,
{
...initialShape,
...TLDR.transform(next, shape, newShapeBounds, {
type: this.transformType, type: this.transformType,
initialShape, initialShape,
scaleX: this.scaleX, scaleX: this.scaleX,
scaleY: this.scaleY, scaleY: this.scaleY,
transformOrigin, transformOrigin,
}), })
}, })
]
}
)
),
}
return { return {
page: next.page, document: {
pages: {
[data.appState.currentPageId]: {
shapes,
},
},
},
} }
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { shapeBounds } = this.snapshot const { shapeBounds } = this.snapshot
const shapes = {} as Record<string, TLDrawShape>
shapeBounds.forEach((shape) => (shapes[shape.id] = shape.initialShape))
return { return {
page: { document: {
...data.page, pages: {
shapes: { [data.appState.currentPageId]: {
...data.page.shapes, shapes,
...Object.fromEntries( },
Object.entries(shapeBounds).map(([id, { initialShape }]) => [id, initialShape])
),
}, },
}, },
} }
@ -116,23 +99,32 @@ export class TransformSession implements Session {
if (!hasUnlockedShapes) return data if (!hasUnlockedShapes) return data
const beforeShapes = {} as Record<string, TLDrawShape>
const afterShapes = {} as Record<string, TLDrawShape>
shapeBounds.forEach((shape) => {
beforeShapes[shape.id] = shape.initialShape
afterShapes[shape.id] = TLDR.getShape(data, shape.id)
})
return { return {
id: 'transform', id: 'transform',
before: { before: {
page: { document: {
shapes: Object.fromEntries( pages: {
Object.entries(shapeBounds).map(([id, { initialShape }]) => [id, initialShape]) [data.appState.currentPageId]: {
), shapes: beforeShapes,
},
},
}, },
}, },
after: { after: {
page: { document: {
shapes: Object.fromEntries( pages: {
this.snapshot.initialShapes.map((shape) => [ [data.appState.currentPageId]: {
shape.id, shapes: afterShapes,
TLDR.onSessionComplete(data, data.page.shapes[shape.id]), },
]) },
),
}, },
}, },
} }
@ -166,24 +158,20 @@ export function getTransformSnapshot(data: Data, transformType: TLBoundsEdge | T
isAllAspectRatioLocked, isAllAspectRatioLocked,
initialShapes, initialShapes,
initialBounds: commonBounds, initialBounds: commonBounds,
shapeBounds: Object.fromEntries( shapeBounds: initialShapes.map((shape) => {
initialShapes.map((shape) => {
const initialShapeBounds = shapesBounds[shape.id] const initialShapeBounds = shapesBounds[shape.id]
const ic = Utils.getBoundsCenter(initialShapeBounds) const ic = Utils.getBoundsCenter(initialShapeBounds)
const ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width const ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
const iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height const iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
return [ return {
shape.id, id: shape.id,
{
initialShape: shape, initialShape: shape,
initialShapeBounds, initialShapeBounds,
transformOrigin: [ix, iy], transformOrigin: [ix, iy],
}, }
] }),
})
),
} }
} }

View file

@ -1,4 +1,4 @@
import { Utils, Vec } from '@tldraw/core' import { TLPageState, Utils, Vec } from '@tldraw/core'
import { import {
TLDrawShape, TLDrawShape,
TLDrawBinding, TLDrawBinding,
@ -7,6 +7,7 @@ import {
Data, Data,
Command, Command,
TLDrawStatus, TLDrawStatus,
ShapesWithProp,
} from '~types' } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
@ -24,35 +25,32 @@ export class TranslateSession implements Session {
this.snapshot = getTranslateSnapshot(data) this.snapshot = getTranslateSnapshot(data)
} }
start = (data: Data): Partial<Data> => { start = (data: Data) => {
const { bindingsToDelete } = this.snapshot const { bindingsToDelete } = this.snapshot
if (bindingsToDelete.length === 0) return data if (bindingsToDelete.length === 0) return data
const nextBindings = { ...data.page.bindings } const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
bindingsToDelete.forEach((binding) => delete nextBindings[binding.id]) bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
const nextShapes = { ...data.page.shapes }
return { return {
page: { document: {
...data.page, pages: {
shapes: nextShapes, [data.appState.currentPageId]: {
bindings: nextBindings, bindings: nextBindings,
}, },
},
},
} }
} }
update = (data: Data, point: number[], isAligned = false, isCloning = false) => { update = (data: Data, point: number[], isAligned = false, isCloning = false) => {
const { clones, initialShapes } = this.snapshot const { clones, initialShapes } = this.snapshot
const next = { const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
...data, const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
page: { ...data.page }, const nextPageState: Partial<TLPageState> = {}
shapes: { ...data.page.shapes },
pageState: { ...data.pageState },
}
const delta = Vec.sub(point, this.origin) const delta = Vec.sub(point, this.origin)
@ -77,105 +75,92 @@ export class TranslateSession implements Session {
// Move original shapes back to start // Move original shapes back to start
next.page.shapes = { initialShapes.forEach((shape) => (nextShapes[shape.id] = { point: shape.point }))
...next.page.shapes,
...Object.fromEntries(
initialShapes.map((shape) => [
shape.id,
{ ...next.page.shapes[shape.id], point: shape.point },
])
),
}
next.page.shapes = { clones.forEach(
...next.page.shapes, (shape) =>
...Object.fromEntries( (nextShapes[shape.id] = { ...shape, point: Vec.round(Vec.add(shape.point, delta)) })
clones.map((clone) => [ )
clone.id,
{ ...clone, point: Vec.round(Vec.add(clone.point, delta)) },
])
),
}
next.pageState.selectedIds = clones.map((c) => c.id) nextPageState.selectedIds = clones.map((shape) => shape.id)
for (const binding of this.snapshot.clonedBindings) { for (const binding of this.snapshot.clonedBindings) {
next.page.bindings[binding.id] = binding nextBindings[binding.id] = binding
} }
next.page.bindings = { ...next.page.bindings }
} }
// Either way, move the clones // Either way, move the clones
next.page.shapes = { clones.forEach((shape) => {
...next.page.shapes, const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id)
...Object.fromEntries(
clones.map((clone) => [
clone.id,
{
...clone,
point: Vec.round(Vec.add(next.page.shapes[clone.id].point, trueDelta)),
},
])
),
}
return { page: { ...next.page }, pageState: { ...next.pageState } } if (!current.point) throw Error('No point on that clone!')
}
nextShapes[shape.id] = {
...nextShapes[shape.id],
point: Vec.round(Vec.add(current.point, trueDelta)),
}
})
} else {
// If not cloning... // If not cloning...
// Cloning -> Not Cloning // Cloning -> Not Cloning
if (this.isCloning) { if (this.isCloning) {
this.isCloning = false this.isCloning = false
next.page.shapes = { ...next.page.shapes }
// Delete the clones // Delete the clones
clones.forEach((clone) => delete next.page.shapes[clone.id]) clones.forEach((clone) => (nextShapes[clone.id] = undefined))
// Move the original shapes back to the cursor position // Move the original shapes back to the cursor position
initialShapes.forEach((shape) => { initialShapes.forEach((shape) => {
next.page.shapes[shape.id] = { nextShapes[shape.id] = {
...next.page.shapes[shape.id],
point: Vec.round(Vec.add(shape.point, delta)), point: Vec.round(Vec.add(shape.point, delta)),
} }
}) })
// Delete the cloned bindings // Delete the cloned bindings
next.page.bindings = { ...next.page.bindings }
for (const binding of this.snapshot.clonedBindings) { for (const binding of this.snapshot.clonedBindings) {
delete next.page.bindings[binding.id] nextBindings[binding.id] = undefined
} }
// Set selected ids // Set selected ids
next.pageState.selectedIds = initialShapes.map((c) => c.id) nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
} }
// Move the shapes by the delta // Move the shapes by the delta
next.page.shapes = { initialShapes.forEach((shape) => {
...next.page.shapes, const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id)
...Object.fromEntries(
initialShapes.map((shape) => [ if (!current.point) throw Error('No point on that clone!')
shape.id,
{ nextShapes[shape.id] = {
...next.page.shapes[shape.id], ...nextShapes[shape.id],
point: Vec.round(Vec.add(next.page.shapes[shape.id].point, trueDelta)), point: Vec.round(Vec.add(current.point, trueDelta)),
}, }
]) })
),
} }
return { page: { ...next.page }, pageState: { ...next.pageState } } return {
document: {
pages: {
[data.appState.currentPageId]: {
shapes: nextShapes,
bindings: nextBindings,
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
},
}
} }
cancel = (data: Data) => { cancel = (data: Data) => {
const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot
const nextShapes: Record<string, TLDrawShape> = { ...data.page.shapes } const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const nextBindings = { ...data.page.bindings } const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
const nextPageState: Partial<TLPageState> = {}
// Put back any deleted bindings // Put back any deleted bindings
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = binding)) bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = binding))
@ -184,20 +169,24 @@ export class TranslateSession implements Session {
initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point })) initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point }))
// Delete clones // Delete clones
clones.forEach((clone) => delete nextShapes[clone.id]) clones.forEach((clone) => (nextShapes[clone.id] = undefined))
// Delete cloned bindings // Delete cloned bindings
clonedBindings.forEach((binding) => delete nextBindings[binding.id]) clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
nextPageState.selectedIds = this.snapshot.selectedIds
return { return {
page: { document: {
...data.page, pages: {
[data.appState.currentPageId]: {
shapes: nextShapes, shapes: nextShapes,
bindings: nextBindings, bindings: nextBindings,
}, },
pageState: { pageStates: {
...data.pageState, [data.appState.currentPageId]: nextPageState,
selectedIds: this.snapshot.selectedIds, },
},
}, },
} }
} }
@ -205,36 +194,33 @@ export class TranslateSession implements Session {
complete(data: Data): Command { complete(data: Data): Command {
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot
const before: PagePartial = { const beforeBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
shapes: { const beforeShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
...Object.fromEntries(clones.map((clone) => [clone.id, undefined])),
...Object.fromEntries(initialShapes.map((shape) => [shape.id, { point: shape.point }])),
},
bindings: {
...Object.fromEntries(clonedBindings.map((binding) => [binding.id, undefined])),
...Object.fromEntries(bindingsToDelete.map((binding) => [binding.id, binding])),
},
}
const after: PagePartial = { const afterBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
shapes: { const afterShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
...Object.fromEntries(clones.map((clone) => [clone.id, data.page.shapes[clone.id]])),
...Object.fromEntries( clones.forEach((shape) => {
initialShapes.map((shape) => [shape.id, { point: data.page.shapes[shape.id].point }]) beforeShapes[shape.id] = undefined
), afterShapes[shape.id] = this.isCloning ? TLDR.getShape(data, shape.id) : undefined
}, })
bindings: {
...Object.fromEntries( initialShapes.forEach((shape) => {
clonedBindings.map((binding) => [binding.id, data.page.bindings[binding.id]]) beforeShapes[shape.id] = { point: shape.point }
), afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id).point }
...Object.fromEntries(bindingsToDelete.map((binding) => [binding.id, undefined])), })
},
} clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id)
})
bindingsToDelete.forEach((binding) => { bindingsToDelete.forEach((binding) => {
beforeBindings[binding.id] = binding
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 = data.page.shapes[id] const shape = TLDR.getShape(data, id)
// 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
@ -242,17 +228,26 @@ export class TranslateSession implements Session {
Object.values(shape.handles) Object.values(shape.handles)
.filter((handle) => handle.bindingId === binding.id) .filter((handle) => handle.bindingId === binding.id)
.forEach((handle) => { .forEach((handle) => {
before.shapes[id] = { let shape: Partial<TLDrawShape> | undefined = beforeShapes[id]
...before.shapes[id], if (!shape) shape = {}
handles: {
...before.shapes[id]?.handles, TLDR.assertShapeHasProperty(shape as TLDrawShape, 'handles')
[handle.id]: { bindingId: binding.id },
}, if (!beforeShapes[id]) {
beforeShapes[id] = { handles: {} }
} }
after.shapes[id] = {
...after.shapes[id], if (!afterShapes[id]) {
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } }, afterShapes[id] = { handles: {} }
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
beforeShapes[id].handles[handle.id] = { bindingId: binding.id }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
beforeShapes[id].handles[handle.id] = { bindingId: undefined }
}) })
} }
}) })
@ -260,15 +255,33 @@ export class TranslateSession implements Session {
return { return {
id: 'translate', id: 'translate',
before: { before: {
page: before, document: {
pageState: { pages: {
[data.appState.currentPageId]: {
shapes: beforeShapes,
bindings: beforeBindings,
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [...this.snapshot.selectedIds], selectedIds: [...this.snapshot.selectedIds],
}, },
}, },
},
},
after: { after: {
page: after, document: {
pageState: { pages: {
selectedIds: [...data.pageState.selectedIds], [data.appState.currentPageId]: {
shapes: afterShapes,
bindings: afterBindings,
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [...TLDR.getSelectedIds(data)],
},
},
}, },
}, },
} }
@ -283,8 +296,10 @@ export function getTranslateSnapshot(data: Data) {
const cloneMap: Record<string, string> = {} const cloneMap: Record<string, string> = {}
const page = TLDR.getPage(data)
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 !== data.page.id) .filter((id) => id !== page.id)
.map((id) => { .map((id) => {
const shape = TLDR.getShape(data, id) const shape = TLDR.getShape(data, id)
return { return {
@ -315,7 +330,7 @@ export function getTranslateSnapshot(data: Data) {
for (const handle of Object.values(shape.handles)) { for (const handle of Object.values(shape.handles)) {
if (handle.bindingId) { if (handle.bindingId) {
const binding = data.page.bindings[handle.bindingId] const binding = page.bindings[handle.bindingId]
const cloneBinding = { const cloneBinding = {
...binding, ...binding,
id: Utils.uniqueId(), id: Utils.uniqueId(),

View file

@ -1,4 +1,4 @@
import { TLBounds, TLTransformInfo, Vec, Utils } from '@tldraw/core' import { TLBounds, TLTransformInfo, Vec, Utils, TLPageState } from '@tldraw/core'
import { getShapeUtils } from '~shape' import { getShapeUtils } from '~shape'
import type { import type {
Data, Data,
@ -8,6 +8,7 @@ import type {
TLDrawShape, TLDrawShape,
TLDrawShapeUtil, TLDrawShapeUtil,
TLDrawBinding, TLDrawBinding,
TLDrawPage,
} from '~types' } from '~types'
export class TLDR { export class TLDR {
@ -16,11 +17,13 @@ export class TLDR {
} }
static getSelectedShapes(data: Data) { static getSelectedShapes(data: Data) {
return data.pageState.selectedIds.map((id) => data.page.shapes[id]) const page = this.getPage(data)
const selectedIds = this.getSelectedIds(data)
return selectedIds.map((id) => page.shapes[id])
} }
static screenToWorld(data: Data, point: number[]) { static screenToWorld(data: Data, point: number[]) {
const { camera } = data.pageState const camera = this.getCamera(data)
return Vec.sub(Vec.div(point, camera.zoom), camera.point) return Vec.sub(Vec.div(point, camera.zoom), camera.point)
} }
@ -42,32 +45,32 @@ export class TLDR {
return Utils.clamp(zoom, 0.1, 5) return Utils.clamp(zoom, 0.1, 5)
} }
static getCurrentCamera(data: Data) { static getPage(data: Data, pageId = data.appState.currentPageId): TLDrawPage {
return data.pageState.camera return data.document.pages[pageId]
} }
static getPage(data: Data) { static getPageState(data: Data, pageId = data.appState.currentPageId): TLPageState {
return data.page return data.document.pageStates[pageId]
} }
static getPageState(data: Data) { static getSelectedIds(data: Data, pageId = data.appState.currentPageId): string[] {
return data.pageState return this.getPageState(data, pageId).selectedIds
} }
static getSelectedIds(data: Data) { static getShapes(data: Data, pageId = data.appState.currentPageId): TLDrawShape[] {
return data.pageState.selectedIds return Object.values(this.getPage(data, pageId).shapes)
} }
static getShapes(data: Data) { static getCamera(data: Data, pageId = data.appState.currentPageId): TLPageState['camera'] {
return Object.values(data.page.shapes) return this.getPageState(data, pageId).camera
} }
static getCamera(data: Data) { static getShape<T extends TLDrawShape = TLDrawShape>(
return data.pageState.camera data: Data,
} shapeId: string,
pageId = data.appState.currentPageId
static getShape<T extends TLDrawShape = TLDrawShape>(data: Data, shapeId: string): T { ): T {
return data.page.shapes[shapeId] as T return this.getPage(data, pageId).shapes[shapeId] as T
} }
static getBounds<T extends TLDrawShape>(shape: T) { static getBounds<T extends TLDrawShape>(shape: T) {
@ -85,24 +88,26 @@ export class TLDR {
} }
static getParentId(data: Data, id: string) { static getParentId(data: Data, id: string) {
const shape = data.page.shapes[id] return this.getShape(data, id).parentId
return shape.parentId
} }
static getPointedId(data: Data, id: string): string { static getPointedId(data: Data, id: string): string {
const shape = data.page.shapes[id] const page = this.getPage(data)
const pageState = this.getPageState(data)
const shape = this.getShape(data, id)
if (!shape) return id if (!shape) return id
return shape.parentId === data.pageState.currentParentId || shape.parentId === data.page.id return shape.parentId === pageState.currentParentId || shape.parentId === page.id
? id ? id
: this.getPointedId(data, shape.parentId) : this.getPointedId(data, shape.parentId)
} }
static getDrilledPointedId(data: Data, id: string): string { static getDrilledPointedId(data: Data, id: string): string {
const shape = data.page.shapes[id] const shape = this.getShape(data, id)
const { currentParentId, pointedId } = data.pageState const { currentPageId } = data.appState
const { currentParentId, pointedId } = this.getPageState(data)
return shape.parentId === data.page.id || return shape.parentId === currentPageId ||
shape.parentId === pointedId || shape.parentId === pointedId ||
shape.parentId === currentParentId shape.parentId === currentParentId
? id ? id
@ -110,20 +115,22 @@ export class TLDR {
} }
static getTopParentId(data: Data, id: string): string { static getTopParentId(data: Data, id: string): string {
const shape = data.page.shapes[id] const page = this.getPage(data)
const pageState = this.getPageState(data)
const shape = this.getShape(data, id)
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}`)
} }
return shape.parentId === data.page.id || shape.parentId === data.pageState.currentParentId return shape.parentId === page.id || shape.parentId === pageState.currentParentId
? id ? id
: this.getTopParentId(data, shape.parentId) : this.getTopParentId(data, shape.parentId)
} }
// 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): string[] {
const shape = data.page.shapes[id] const shape = this.getShape(data, id)
if (shape.children === undefined) return [id] if (shape.children === undefined) return [id]
@ -178,10 +185,12 @@ 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[]): string[] {
const page = this.getPage(data)
const visited = new Set(ids) const visited = new Set(ids)
ids.forEach((id) => { ids.forEach((id) => {
const shape = data.page.shapes[id] const shape = page.shapes[id]
// Add descendant shapes // Add descendant shapes
function collectDescendants(shape: TLDrawShape): void { function collectDescendants(shape: TLDrawShape): void {
@ -190,7 +199,7 @@ export class TLDR {
.filter((childId) => !visited.has(childId)) .filter((childId) => !visited.has(childId))
.forEach((childId) => { .forEach((childId) => {
visited.add(childId) visited.add(childId)
collectDescendants(data.page.shapes[childId]) collectDescendants(page.shapes[childId])
}) })
} }
@ -199,17 +208,17 @@ export class TLDR {
// Add asecendant shapes // Add asecendant shapes
function collectAscendants(shape: TLDrawShape): void { function collectAscendants(shape: TLDrawShape): void {
const parentId = shape.parentId const parentId = shape.parentId
if (parentId === data.page.id) return if (parentId === page.id) return
if (visited.has(parentId)) return if (visited.has(parentId)) return
visited.add(parentId) visited.add(parentId)
collectAscendants(data.page.shapes[parentId]) collectAscendants(page.shapes[parentId])
} }
collectAscendants(shape) collectAscendants(shape)
// Add bindings that are to or from any of the visited shapes (this does not have to be recursive) // Add bindings that are to or from any of the visited shapes (this does not have to be recursive)
visited.forEach((id) => { visited.forEach((id) => {
Object.values(data.page.bindings) Object.values(page.bindings)
.filter((binding) => binding.fromId === id || binding.toId === id) .filter((binding) => binding.fromId === id || binding.toId === id)
.forEach((binding) => visited.add(binding.fromId === id ? binding.toId : binding.fromId)) .forEach((binding) => visited.add(binding.fromId === id ? binding.toId : binding.fromId))
}) })
@ -225,31 +234,29 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {}
): Data { ): Data {
const shape = data.page.shapes[id] as T const page = this.getPage(data)
const shape = page.shapes[id] as T
if (shape.children !== undefined) { if (shape.children !== undefined) {
const deltas = this.getShapeUtils(shape).updateChildren( const deltas = this.getShapeUtils(shape).updateChildren(
shape, shape,
shape.children.map((childId) => data.page.shapes[childId]) shape.children.map((childId) => page.shapes[childId])
) )
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 deltaShape = this.getShape(cData, delta.id)
const deltaShape = cData.page.shapes[delta.id] if (!beforeShapes[delta.id]) {
beforeShapes[delta.id] = deltaShape
if (!beforeShapes[deltaShape.id]) {
beforeShapes[deltaShape.id] = deltaShape
} }
cData.page.shapes[deltaShape.id] = this.getShapeUtils(deltaShape).mutate( cPage.shapes[delta.id] = this.getShapeUtils(deltaShape).mutate(deltaShape, delta)
deltaShape, afterShapes[delta.id] = cPage.shapes[delta.id]
delta
)
afterShapes[deltaShape.id] = cData.page.shapes[deltaShape.id]
if (deltaShape.children !== undefined) { if (deltaShape.children !== undefined) {
this.recursivelyUpdateChildren(cData, deltaShape.id, beforeShapes, afterShapes) this.recursivelyUpdateChildren(cData, delta.id, beforeShapes, afterShapes)
} }
return cData return cData
@ -266,32 +273,50 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {}
): Data { ): Data {
const shape = data.page.shapes[id] as T const page = { ...this.getPage(data) }
const shape = this.getShape<T>(data, id)
if (shape.parentId !== data.page.id) { if (page.id === 'doc') {
const parent = data.page.shapes[shape.parentId] as T throw Error('wtf')
}
if (shape.parentId !== page.id) {
const parent = this.getShape(data, shape.parentId)
if (!parent.children) throw Error('No children in parent!') if (!parent.children) throw Error('No children in parent!')
const delta = this.getShapeUtils(shape).onChildrenChange( const delta = this.getShapeUtils(parent).onChildrenChange(
parent, parent,
parent.children.map((childId) => data.page.shapes[childId]) parent.children.map((childId) => this.getShape(data, childId))
) )
if (delta) { if (delta) {
if (!beforeShapes[parent.id]) { if (!beforeShapes[parent.id]) {
beforeShapes[parent.id] = parent beforeShapes[parent.id] = parent
} }
data.page.shapes[parent.id] = this.getShapeUtils(parent).mutate(parent, delta) page.shapes[parent.id] = this.getShapeUtils(parent).mutate(parent, delta)
afterShapes[parent.id] = data.page.shapes[parent.id] afterShapes[parent.id] = page.shapes[parent.id]
} }
if (parent.parentId !== data.page.id) { if (parent.parentId !== page.id) {
return this.recursivelyUpdateParents(data, parent.parentId, beforeShapes, afterShapes) return this.recursivelyUpdateParents(data, parent.parentId, beforeShapes, afterShapes)
} }
} }
return data if (data.appState.currentPageId === 'doc') {
console.error('WTF?')
}
return {
...data,
document: {
...data.document,
pages: {
...data.document.pages,
[page.id]: page,
},
},
}
} }
static updateBindings( static updateBindings(
@ -300,26 +325,27 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {}, beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {} afterShapes: Record<string, Partial<TLDrawShape>> = {}
): Data { ): Data {
return Object.values(data.page.bindings) const page = { ...this.getPage(data) }
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(cData.page.shapes[binding.fromId]) beforeShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId))
} }
if (!beforeShapes[binding.toId]) { if (!beforeShapes[binding.toId]) {
beforeShapes[binding.toId] = Utils.deepClone(cData.page.shapes[binding.toId]) beforeShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId))
} }
this.onBindingChange( this.onBindingChange(
cData, cData,
cData.page.shapes[binding.fromId], this.getShape(cData, binding.fromId),
binding, binding,
cData.page.shapes[binding.toId] this.getShape(cData, binding.toId)
) )
afterShapes[binding.fromId] = Utils.deepClone(cData.page.shapes[binding.fromId]) afterShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId))
afterShapes[binding.toId] = Utils.deepClone(cData.page.shapes[binding.toId]) afterShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId))
return cData return cData
}, data) }, data)
@ -357,34 +383,28 @@ export class TLDR {
/* Mutations */ /* Mutations */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static setSelectedIds(data: Data, ids: string[]) {
data.pageState.selectedIds = ids
}
static deselectAll(data: Data) {
this.setSelectedIds(data, [])
}
static mutateShapes<T extends TLDrawShape>( static mutateShapes<T extends TLDrawShape>(
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
): { ): {
before: Record<string, Partial<T>> before: Record<string, Partial<T>>
after: Record<string, Partial<T>> after: Record<string, Partial<T>>
data: Data data: Data
} { } {
const page = { ...this.getPage(data, pageId) }
const beforeShapes: Record<string, Partial<T>> = {} const beforeShapes: Record<string, Partial<T>> = {}
const afterShapes: Record<string, Partial<T>> = {} const afterShapes: Record<string, Partial<T>> = {}
ids.forEach((id, i) => { ids.forEach((id, i) => {
const shape = this.getShape<T>(data, id) const shape = this.getShape<T>(data, id, pageId)
const change = fn(shape, i) const change = fn(shape, i)
beforeShapes[id] = Object.fromEntries( beforeShapes[id] = Object.fromEntries(
Object.keys(change).map((key) => [key, shape[key as keyof T]]) Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T> ) as Partial<T>
afterShapes[id] = change afterShapes[id] = change
data.page.shapes[id] = this.getShapeUtils(shape).mutate(shape as T, change as Partial<T>) page.shapes[id] = this.getShapeUtils(shape).mutate(shape as T, change as Partial<T>)
}) })
const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => { const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => {
@ -408,20 +428,23 @@ export class TLDR {
static createShapes( static createShapes(
data: Data, data: Data,
shapes: TLDrawShape[] shapes: TLDrawShape[],
pageId = data.appState.currentPageId
): { before: DeepPartial<Data>; after: DeepPartial<Data> } { ): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const page = this.getPage(data)
const before: DeepPartial<Data> = { const before: DeepPartial<Data> = {
page: { document: {
pages: {
[pageId]: {
shapes: { shapes: {
...Object.fromEntries( ...Object.fromEntries(
shapes.flatMap((shape) => { shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, undefined]] const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, undefined],
]
// If the shape is a child of another shape, also save that shape // If the shape is a child of another shape, also save that shape
if (shape.parentId !== data.page.id) { if (shape.parentId !== pageId) {
const parent = page.shapes[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!')
results.push([parent.id, { children: parent.children }]) results.push([parent.id, { children: parent.children }])
} }
@ -431,18 +454,25 @@ export class TLDR {
), ),
}, },
}, },
},
},
} }
const after: DeepPartial<Data> = { const after: DeepPartial<Data> = {
page: { document: {
pages: {
[pageId]: {
shapes: {
shapes: { shapes: {
...Object.fromEntries( ...Object.fromEntries(
shapes.flatMap((shape) => { shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]] const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, shape],
]
// If the shape is a child of a different shape, update its parent // If the shape is a child of a different shape, update its parent
if (shape.parentId !== data.page.id) { if (shape.parentId !== pageId) {
const parent = page.shapes[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!')
results.push([parent.id, { children: [...parent.children, shape.id] }]) results.push([parent.id, { children: [...parent.children, shape.id] }])
} }
@ -452,6 +482,9 @@ export class TLDR {
), ),
}, },
}, },
},
},
},
} }
return { return {
@ -462,9 +495,10 @@ export class TLDR {
static deleteShapes( static deleteShapes(
data: Data, data: Data,
shapes: TLDrawShape[] | string[] shapes: TLDrawShape[] | string[],
pageId = data.appState.currentPageId
): { before: DeepPartial<Data>; after: DeepPartial<Data> } { ): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const page = this.getPage(data) const page = this.getPage(data, pageId)
const shapeIds = const shapeIds =
typeof shapes[0] === 'string' typeof shapes[0] === 'string'
@ -472,7 +506,9 @@ export class TLDR {
: (shapes as TLDrawShape[]).map((shape) => shape.id) : (shapes as TLDrawShape[]).map((shape) => shape.id)
const before: DeepPartial<Data> = { const before: DeepPartial<Data> = {
page: { document: {
pages: {
[pageId]: {
shapes: { shapes: {
// These are the shapes that we're going to delete // These are the shapes that we're going to delete
...Object.fromEntries( ...Object.fromEntries(
@ -481,7 +517,7 @@ export class TLDR {
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]] const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]]
// If the shape is a child of another shape, also add that shape // If the shape is a child of another shape, also add that shape
if (shape.parentId !== data.page.id) { if (shape.parentId !== pageId) {
const parent = page.shapes[shape.parentId] const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!') if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: parent.children }]) results.push([parent.id, { children: parent.children }])
@ -504,18 +540,24 @@ export class TLDR {
), ),
}, },
}, },
},
},
} }
const after: DeepPartial<Data> = { const after: DeepPartial<Data> = {
page: { document: {
pages: {
[pageId]: {
shapes: { shapes: {
...Object.fromEntries( ...Object.fromEntries(
shapeIds.flatMap((id) => { shapeIds.flatMap((id) => {
const shape = page.shapes[id] const shape = page.shapes[id]
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, undefined]] const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, undefined],
]
// If the shape is a child of a different shape, update its parent // If the shape is a child of a different shape, update its parent
if (shape.parentId !== data.page.id) { if (shape.parentId !== page.id) {
const parent = page.shapes[shape.parentId] const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!') if (!parent.children) throw Error('No children in parent!')
@ -531,6 +573,8 @@ export class TLDR {
), ),
}, },
}, },
},
},
} }
return { return {
@ -550,7 +594,7 @@ export class TLDR {
const delta = getShapeUtils(shape).onChildrenChange( const delta = getShapeUtils(shape).onChildrenChange(
shape, shape,
shape.children.map((id) => data.page.shapes[id]) shape.children.map((id) => this.getShape(data, id))
) )
if (!delta) return shape if (!delta) return shape
return this.mutate(data, shape, delta) return this.mutate(data, shape, delta)
@ -598,7 +642,7 @@ export class TLDR {
next = this.onChildrenChange(data, next) || next next = this.onChildrenChange(data, next) || next
} }
data.page.shapes[next.id] = next // data.page.shapes[next.id] = next
return next return next
} }
@ -608,13 +652,15 @@ export class TLDR {
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static updateParents(data: Data, changedShapeIds: string[]): void { static updateParents(data: Data, changedShapeIds: string[]): void {
const page = this.getPage(data)
if (changedShapeIds.length === 0) return if (changedShapeIds.length === 0) return
const { shapes } = this.getPage(data) const { shapes } = this.getPage(data)
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())
).filter((id) => id !== data.page.id) ).filter((id) => id !== page.id)
for (const parentId of parentToUpdateIds) { for (const parentId of parentToUpdateIds) {
const parent = shapes[parentId] const parent = shapes[parentId]
@ -631,18 +677,17 @@ export class TLDR {
static getSelectedStyle(data: Data): ShapeStyles | false { static getSelectedStyle(data: Data): ShapeStyles | false {
const { const {
page,
pageState,
appState: { currentStyle }, appState: { currentStyle },
} = data } = data
const page = this.getPage(data)
const pageState = this.getPageState(data)
if (pageState.selectedIds.length === 0) { if (pageState.selectedIds.length === 0) {
return currentStyle return currentStyle
} }
const shapeStyles = data.pageState.selectedIds.map((id) => { const shapeStyles = pageState.selectedIds.map((id) => page.shapes[id].style)
return page.shapes[id].style
})
const commonStyle = {} as ShapeStyles const commonStyle = {} as ShapeStyles
@ -673,17 +718,17 @@ export class TLDR {
/* Bindings */ /* Bindings */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
static getBinding(data: Data, id: string): TLDrawBinding { static getBinding(data: Data, id: string, pageId = data.appState.currentPageId): TLDrawBinding {
return this.getPage(data).bindings[id] return this.getPage(data, pageId).bindings[id]
} }
static getBindings(data: Data): TLDrawBinding[] { static getBindings(data: Data, pageId = data.appState.currentPageId): TLDrawBinding[] {
const page = this.getPage(data) 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 Object.values(data.page.shapes) return this.getShapes(data)
.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)
@ -699,22 +744,13 @@ export class TLDR {
) )
} }
static createBindings(data: Data, bindings: TLDrawBinding[]): void {
const page = this.getPage(data)
bindings.forEach((binding) => (page.bindings[binding.id] = binding))
}
// static deleteBindings(data: Data, ids: string[]): void {
// if (ids.length === 0) return
// const page = this.getPage(data)
// ids.forEach((id) => delete page.bindings[id])
// }
static getRelatedBindings(data: Data, ids: string[]): TLDrawBinding[] { static getRelatedBindings(data: Data, ids: string[]): TLDrawBinding[] {
const changedShapeIds = new Set(ids) const changedShapeIds = new Set(ids)
const page = this.getPage(data)
// Find all bindings that we need to update // Find all bindings that we need to update
const bindingsArr = Object.values(data.page.bindings) const bindingsArr = Object.values(page.bindings)
// Start with bindings that are directly bound to our changed shapes // Start with bindings that are directly bound to our changed shapes
const bindingsToUpdate = new Set( const bindingsToUpdate = new Set(
@ -751,42 +787,6 @@ export class TLDR {
return Array.from(bindingsToUpdate.values()) return Array.from(bindingsToUpdate.values())
} }
// static cleanupBindings(data: Data, bindings: TLDrawBinding[]) {
// const before: Record<string, Partial<TLDrawShape>> = {}
// const after: Record<string, Partial<TLDrawShape>> = {}
// // We also need to delete bindings that reference the deleted shapes
// Object.values(bindings).forEach((binding) => {
// for (const id of [binding.toId, binding.fromId]) {
// // If the binding references a deleted shape...
// if (after[id] === undefined) {
// // Delete this binding
// before.bindings[binding.id] = binding
// after.bindings[binding.id] = undefined
// // Let's also look at the bound shape...
// const shape = data.page.shapes[id]
// // If the bound shape has a handle that references the deleted binding, delete that reference
// if (shape.handles) {
// Object.values(shape.handles)
// .filter((handle) => handle.bindingId === binding.id)
// .forEach((handle) => {
// before.shapes[id] = {
// ...before.shapes[id],
// handles: { ...before.shapes[id]?.handles, [handle.id]: { bindingId: binding.id } },
// }
// after.shapes[id] = {
// ...after.shapes[id],
// handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
// }
// })
// }
// }
// }
// })
// }
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Assertions */ /* Assertions */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
@ -799,51 +799,4 @@ export class TLDR {
throw new Error() throw new Error()
} }
} }
// static updateBindings(data: Data, changedShapeIds: string[]): void {
// if (changedShapeIds.length === 0) return
// // First gather all bindings that are directly affected by the change
// const firstPassBindings = this.getBindingsWithShapeIds(data, changedShapeIds)
// // Gather all shapes that will be effected by the binding changes
// const effectedShapeIds = Array.from(
// new Set(firstPassBindings.flatMap((binding) => [binding.toId, binding.fromId])).values(),
// )
// // Now get all bindings that are affected by those shapes
// const bindingsToUpdate = this.getBindingsWithShapeIds(data, effectedShapeIds)
// // Populate a map of { [shapeId]: BindingsThatWillEffectTheShape[] }
// // Note that this will include both to and from bindings, and so will
// // likely include ids other than the changedShapeIds provided.
// const shapeIdToBindingsMap = new Map<string, TLDrawBinding[]>()
// bindingsToUpdate.forEach((binding) => {
// const { toId, fromId } = binding
// for (const id of [toId, fromId]) {
// if (!shapeIdToBindingsMap.has(id)) {
// shapeIdToBindingsMap.set(id, [binding])
// } else {
// const bindings = shapeIdToBindingsMap.get(id)
// bindings.push(binding)
// }
// }
// })
// // Update each effected shape with the binding that effects it.
// Array.from(shapeIdToBindingsMap.entries()).forEach(([id, bindings]) => {
// const shape = this.getShape(data, id)
// bindings.forEach((binding) => {
// const otherShape =
// binding.toId === id
// ? this.getShape(data, binding.fromId)
// : this.getShape(data, binding.toId)
// this.onBindingChange(data, shape, binding, otherShape)
// })
// })
// }
} }

View file

@ -108,7 +108,6 @@ export class TLDrawState implements TLCallbacks {
pointedHandle?: string pointedHandle?: string
editingId?: string editingId?: string
pointedBoundsHandle?: TLBoundsCorner | TLBoundsEdge | 'rotate' pointedBoundsHandle?: TLBoundsCorner | TLBoundsEdge | 'rotate'
currentDocumentId = 'doc'
currentPageId = 'page' currentPageId = 'page'
document: TLDrawDocument document: TLDrawDocument
isCreating = false isCreating = false
@ -142,26 +141,29 @@ export class TLDrawState implements TLCallbacks {
// Remove deleted shapes and bindings (in Commands, these will be set to undefined) // Remove deleted shapes and bindings (in Commands, these will be set to undefined)
if (result.document) { if (result.document) {
for (const pageId in result.document.pages) { Object.values(current.document.pages).forEach((currentPage) => {
const currentPage = next.document.pages[pageId] const pageId = currentPage.id
const nextPage = { const nextPage = {
...next.document, ...currentPage,
shapes: { ...currentPage.shapes }, ...result.document?.pages[pageId],
bindings: { ...currentPage.bindings }, shapes: { ...result.document?.pages[pageId]?.shapes },
bindings: { ...result.document?.pages[pageId]?.bindings },
} }
for (const id in nextPage.shapes) { Object.keys(nextPage.shapes).forEach((id) => {
if (!nextPage.shapes[id]) delete nextPage.shapes[id] if (!nextPage.shapes[id]) delete nextPage.shapes[id]
} })
for (const id in nextPage.bindings) { Object.keys(nextPage.bindings).forEach((id) => {
if (!nextPage.bindings[id]) delete nextPage.bindings[id] if (!nextPage.bindings[id]) delete nextPage.bindings[id]
} })
const changedShapeIds = Object.values(nextPage.shapes) const changedShapeIds = Object.values(nextPage.shapes)
.filter((shape) => currentPage.shapes[shape.id] !== shape) .filter((shape) => currentPage.shapes[shape.id] !== shape)
.map((shape) => shape.id) .map((shape) => shape.id)
next.document.pages[pageId] = nextPage
// Get bindings related to the changed shapes // Get bindings related to the changed shapes
const bindingsToUpdate = TLDR.getRelatedBindings(next, changedShapeIds) const bindingsToUpdate = TLDR.getRelatedBindings(next, changedShapeIds)
@ -203,7 +205,7 @@ export class TLDrawState implements TLCallbacks {
} }
if (nextPageState.bindingId && !nextPage.bindings[nextPageState.bindingId]) { if (nextPageState.bindingId && !nextPage.bindings[nextPageState.bindingId]) {
console.warn('Could not find the binding shape!') console.warn('Could not find the binding shape!', pageId)
delete nextPageState.bindingId delete nextPageState.bindingId
} }
@ -214,7 +216,7 @@ export class TLDrawState implements TLCallbacks {
next.document.pages[pageId] = nextPage next.document.pages[pageId] = nextPage
next.document.pageStates[pageId] = nextPageState next.document.pageStates[pageId] = nextPageState
} })
} }
// Apply selected style change, if any // Apply selected style change, if any
@ -245,19 +247,23 @@ export class TLDrawState implements TLCallbacks {
} }
getShape = <T extends TLDrawShape = TLDrawShape>(id: string, pageId = this.currentPageId): T => { getShape = <T extends TLDrawShape = TLDrawShape>(id: string, pageId = this.currentPageId): T => {
return this.document.pages[pageId].shapes[id] as T return TLDR.getShape<T>(this.data, id, pageId)
} }
getPage = (id = this.currentPageId) => { getPage = (pageId = this.currentPageId) => {
return this.document.pages[id] return TLDR.getPage(this.data, pageId)
} }
getShapes = (id = this.currentPageId) => { getShapes = (pageId = this.currentPageId) => {
return Object.values(this.getPage(id).shapes).sort((a, b) => a.childIndex - b.childIndex) return TLDR.getShapes(this.data, pageId)
} }
getPageState = (id = this.currentPageId) => { getBindings = (pageId = this.currentPageId) => {
return this.document.pageStates[id] return TLDR.getBindings(this.data, pageId)
}
getPageState = (pageId = this.currentPageId) => {
return TLDR.getPageState(this.data, pageId)
} }
getAppState = () => { getAppState = () => {
@ -265,7 +271,7 @@ export class TLDrawState implements TLCallbacks {
} }
getPagePoint = (point: number[]) => { getPagePoint = (point: number[]) => {
const { camera } = this.getPageState() const { camera } = this.pageState
return Vec.sub(Vec.div(point, camera.zoom), camera.point) return Vec.sub(Vec.div(point, camera.zoom), camera.point)
} }
@ -598,7 +604,6 @@ export class TLDrawState implements TLCallbacks {
loadDocument = (document: TLDrawDocument, onChange?: TLDrawState['_onChange']) => { loadDocument = (document: TLDrawDocument, onChange?: TLDrawState['_onChange']) => {
this._onChange = onChange this._onChange = onChange
this.currentDocumentId = document.id
this.document = Utils.deepClone(document) this.document = Utils.deepClone(document)
this.currentPageId = Object.keys(document.pages)[0] this.currentPageId = Object.keys(document.pages)[0]
this.selectHistory.pointer = 0 this.selectHistory.pointer = 0
@ -637,7 +642,12 @@ export class TLDrawState implements TLCallbacks {
startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) { startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) {
this.session = session this.session = session
this.setState((data) => session.start(data, ...args), session.status) const result = session.start(this.getState(), ...args)
if (result) {
this.setState((data) => Utils.deepMerge<Data>(data, result), session.status)
} else {
this.setStatus(session.status)
}
this._onChange?.(this, `session:start_${session.id}`) this._onChange?.(this, `session:start_${session.id}`)
return this return this
} }
@ -645,7 +655,7 @@ export class TLDrawState implements TLCallbacks {
updateSession<T extends Session>(...args: ParametersExceptFirst<T['update']>) { updateSession<T extends Session>(...args: ParametersExceptFirst<T['update']>) {
const { session } = this const { session } = this
if (!session) return this if (!session) return this
this.setState((data) => session.update(data, ...args)) this.setState((data) => Utils.deepMerge<Data>(data, session.update(data, ...args)))
this._onChange?.(this, `session:update:${session.id}`) this._onChange?.(this, `session:update:${session.id}`)
return this return this
} }
@ -696,7 +706,10 @@ export class TLDrawState implements TLCallbacks {
this.isCreating = false this.isCreating = false
this._onChange?.(this, `session:cancel_create:${session.id}`) this._onChange?.(this, `session:cancel_create:${session.id}`)
} else { } else {
this.setState((data) => session.cancel(data, ...args), TLDrawStatus.Idle) this.setState(
(data) => Utils.deepMerge<Data>(data, session.cancel(data, ...args)),
TLDrawStatus.Idle
)
this._onChange?.(this, `session:cancel:${session.id}`) this._onChange?.(this, `session:cancel:${session.id}`)
} }
@ -719,7 +732,10 @@ export class TLDrawState implements TLCallbacks {
this.session = undefined this.session = undefined
if ('after' in result) { if (result === undefined) {
this.isCreating = false
this._onChange?.(this, `session:complete:${session.id}`)
} else if ('after' in result) {
// Session ended with a command // Session ended with a command
if (this.isCreating) { if (this.isCreating) {
@ -2108,7 +2124,7 @@ export class TLDrawState implements TLCallbacks {
} }
get bindings() { get bindings() {
return this.getPage().bindings return this.getBindings()
} }
get pageState() { get pageState() {

View file

@ -40,7 +40,10 @@ export interface Data {
status: { current: TLDrawStatus; previous: TLDrawStatus } status: { current: TLDrawStatus; previous: TLDrawStatus }
} }
} }
export type PagePartial = DeepPartial<TLDrawPage> export type PagePartial = {
shapes: DeepPartial<TLDrawPage['shapes']>
bindings: DeepPartial<TLDrawPage['bindings']>
}
export type DeepPartial<T> = T extends Function export type DeepPartial<T> = T extends Function
? T ? T
@ -69,10 +72,10 @@ export interface SelectHistory {
export interface Session { export interface Session {
id: string id: string
status: TLDrawStatus status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => Partial<Data> start: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | void
update: (data: Readonly<Data>, ...args: any[]) => Partial<Data> update: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
complete: (data: Readonly<Data>, ...args: any[]) => Partial<Data> | Command complete: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | Command | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => Partial<Data> cancel: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
} }
export enum TLDrawStatus { export enum TLDrawStatus {

View file

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

View file

@ -1,6 +1,6 @@
import * as React from "react" import * as React from 'react'
import { openDB, DBSchema } from "idb" import { openDB, DBSchema } from 'idb'
import type { TLDrawDocument } from "@tldraw/tldraw" import type { TLDrawDocument } from '@tldraw/tldraw'
interface TLDatabase extends DBSchema { interface TLDatabase extends DBSchema {
documents: { documents: {
@ -9,6 +9,8 @@ interface TLDatabase extends DBSchema {
} }
} }
const VERSION = 2
/** /**
* Persist a value in indexdb. This hook is designed to be used primarily through * Persist a value in indexdb. This hook is designed to be used primarily through
* its methods, `setValue` and `forceUpdate`. The `setValue` method will update the * its methods, `setValue` and `forceUpdate`. The `setValue` method will update the
@ -24,20 +26,20 @@ interface TLDatabase extends DBSchema {
*/ */
export function usePersistence(id: string, doc: TLDrawDocument) { export function usePersistence(id: string, doc: TLDrawDocument) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const [status, setStatus] = React.useState<"loading" | "ready">("loading") const [status, setStatus] = React.useState<'loading' | 'ready'>('loading')
const [value, _setValue] = React.useState<TLDrawDocument | null>(null) const [value, _setValue] = React.useState<TLDrawDocument | null>(null)
// A function that other parts of the program can use to manually update // A function that other parts of the program can use to manually update
// the state to the latest value in the database. // the state to the latest value in the database.
const forceUpdate = React.useCallback(() => { const forceUpdate = React.useCallback(() => {
_setValue(null) _setValue(null)
setStatus("loading") setStatus('loading')
openDB<TLDatabase>("db", 1).then((db) => openDB<TLDatabase>('db', VERSION).then((db) =>
db.get("documents", id).then((v) => { db.get('documents', id).then((v) => {
if (!v) throw Error(`Could not find document with id: ${id}`) if (!v) throw Error(`Could not find document with id: ${id}`)
_setValue(v) _setValue(v)
setStatus("ready") setStatus('ready')
}) })
) )
}, [id]) }, [id])
@ -46,7 +48,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// value in the database. // value in the database.
const setValue = React.useCallback( const setValue = React.useCallback(
(doc: TLDrawDocument) => { (doc: TLDrawDocument) => {
openDB<TLDatabase>("db", 1).then((db) => db.put("documents", doc, id)) openDB<TLDatabase>('db', VERSION).then((db) => db.put('documents', doc, id))
}, },
[id] [id]
) )
@ -55,17 +57,17 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state. // the state.
React.useEffect(() => { React.useEffect(() => {
async function handleLoad() { async function handleLoad() {
const db = await openDB<TLDatabase>("db", 1, { const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) { upgrade(db) {
db.createObjectStore("documents") db.createObjectStore('documents')
}, },
}) })
let savedDoc: TLDrawDocument let savedDoc: TLDrawDocument
try { try {
const restoredDoc = await db.get("documents", id) const restoredDoc = await db.get('documents', id)
if (!restoredDoc) throw Error("No document") if (!restoredDoc) throw Error('No document')
savedDoc = restoredDoc savedDoc = restoredDoc
restoredDoc.pageStates = Object.fromEntries( restoredDoc.pageStates = Object.fromEntries(
Object.entries(restoredDoc.pageStates).map(([pageId, pageState]) => [ Object.entries(restoredDoc.pageStates).map(([pageId, pageState]) => [
@ -78,12 +80,12 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
]) ])
) )
} catch (e) { } catch (e) {
await db.put("documents", doc, id) await db.put('documents', doc, id)
savedDoc = doc savedDoc = doc
} }
_setValue(savedDoc) _setValue(savedDoc)
setStatus("ready") setStatus('ready')
} }
handleLoad() handleLoad()