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

View file

@ -32,13 +32,13 @@ import {
} from '@radix-ui/react-icons'
const has1SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 0
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
}
const has2SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 1
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
}
const has3SelectedIdsSelector = (s: Data) => {
return s.pageState.selectedIds.length > 2
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
}
const isDebugModeSelector = (s: Data) => {
@ -46,7 +46,9 @@ const isDebugModeSelector = (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 {

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import { strokes } from '~shape'
import { useTheme, useTLDrawContext } from '~hooks'
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 => {
const { theme } = useTheme()

View file

@ -19,7 +19,7 @@ const dashes = {
[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 => {
const { tlstate, useSelector } = useTLDrawContext()

View file

@ -5,7 +5,7 @@ import { breakpoints, Tooltip, IconButton, IconWrapper } from '../shared'
import { useTLDrawContext } from '~hooks'
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 => {
const { tlstate, useSelector } = useTLDrawContext()

View file

@ -12,7 +12,7 @@ const sizes = {
[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 => {
const { tlstate, useSelector } = useTLDrawContext()

View file

@ -18,17 +18,23 @@ import { useTLDrawContext } from '~hooks'
import type { Data } from '~types'
const isAllLockedSelector = (s: Data) => {
const { selectedIds } = s.pageState
return selectedIds.every((id) => s.page.shapes[id].isLocked)
const page = s.document.pages[s.appState.currentPageId]
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.every((id) => page.shapes[id].isLocked)
}
const isAllAspectLockedSelector = (s: Data) => {
const { selectedIds } = s.pageState
return selectedIds.every((id) => s.page.shapes[id].isAspectRatioLocked)
const page = s.document.pages[s.appState.currentPageId]
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
}
const isAllGroupedSelector = (s: Data) => {
const 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(
(shape) =>
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(() => {
const { tlstate, useSelector } = useTLDrawContext()

View file

@ -50,7 +50,9 @@ export function StylePanel(): JSX.Element {
}
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 {
const { tlstate, useSelector } = useTLDrawContext()

View file

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

View file

@ -5,7 +5,8 @@ import type { Data } from '~types'
import { useTLDrawContext } from '~hooks'
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(() => {
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() {
const { tlstate, useSelector } = useTLDrawContext()

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import { TLDR } from '~state/tldr'
import type { Data, Command, PagePartial } from '~types'
// - [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
ids.forEach((id) => {
before.shapes[id] = data.page.shapes[id]
before.shapes[id] = TLDR.getShape(data, id)
after.shapes[id] = undefined
})
const page = TLDR.getPage(data)
// 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]) {
// If the binding references a deleted shape...
if (after.shapes[id] === undefined) {
@ -31,7 +34,7 @@ export function deleteShapes(data: Data, ids: string[]): Command {
after.bindings[binding.id] = undefined
// 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 (shape.handles) {
@ -55,15 +58,23 @@ export function deleteShapes(data: Data, ids: string[]): Command {
return {
id: 'delete_shapes',
before: {
page: before,
pageState: {
selectedIds: [...data.pageState.selectedIds],
document: {
pages: {
[data.appState.currentPageId]: before,
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: TLDR.getSelectedIds(data) },
},
},
},
after: {
page: after,
pageState: {
selectedIds: [],
document: {
pages: {
[data.appState.currentPageId]: after,
},
pageStates: {
[data.appState.currentPageId]: { selectedIds: [] },
},
},
},
}

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { Utils } from '@tldraw/core'
import type { Data } from '~types'
import { TLDR } from '~state/tldr'
const doc = Utils.deepClone(mockDocument)
@ -32,7 +33,7 @@ delete doc.pages.page1.shapes['rect2']
delete doc.pages.page1.shapes['rect3']
function getSortedShapeIds(data: Data) {
return Object.values(data.page.shapes)
return TLDR.getShapes(data)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id)
.join('')

View file

@ -3,27 +3,30 @@ import { TLDR } from '~state/tldr'
export function move(data: Data, ids: string[], type: MoveType): Command {
// 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: {
before: Record<string, Partial<TLDrawShape>>
after: Record<string, Partial<TLDrawShape>>
} = { before: {}, after: {} }
let startIndex: number
let startChildIndex: number
let step: number
const page = TLDR.getPage(data)
// Collect shapes with common parents into a table under their parent id
Array.from(parentIds.values()).forEach((parentId) => {
let sortedChildren: TLDrawShape[] = []
if (parentId === data.page.id) {
sortedChildren = Object.values(data.page.shapes).sort((a, b) => a.childIndex - b.childIndex)
if (parentId === page.id) {
sortedChildren = Object.values(page.shapes).sort((a, b) => a.childIndex - b.childIndex)
} else {
const parent = data.page.shapes[parentId]
const parent = TLDR.getShape(data, parentId)
if (!parent.children) throw Error('No children in parent!')
sortedChildren = parent.children
.map((childId) => data.page.shapes[childId])
.map((childId) => TLDR.getShape(data, childId))
.sort((a, b) => a.childIndex - b.childIndex)
}
@ -197,15 +200,17 @@ export function move(data: Data, ids: string[], type: MoveType): Command {
return {
id: 'move_shapes',
before: {
page: {
...data.page,
shapes: result?.before || {},
document: {
pages: {
[data.appState.currentPageId]: { shapes: result.before },
},
},
},
after: {
page: {
...data.page,
shapes: result?.after || {},
document: {
pages: {
[data.appState.currentPageId]: { shapes: result.after },
},
},
},
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Command {
const initialShapes = ids.map((id) => data.page.shapes[id])
const initialShapes = ids.map((id) => TLDR.getShape(data, id))
const isAllToggled = initialShapes.every((shape) => shape[prop])
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 {
id: 'toggle_shapes',
before: {
page: {
shapes: {
...before,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: before,
},
},
},
},
after: {
page: {
shapes: {
...after,
document: {
pages: {
[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]) {
// 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 (!shape.handles) continue
@ -54,15 +54,17 @@ export function translate(data: Data, ids: string[], delta: number[]): Command {
return {
id: 'translate_shapes',
before: {
page: {
...data.page,
...before,
document: {
pages: {
[data.appState.currentPageId]: before,
},
},
},
after: {
page: {
...data.page,
...after,
document: {
pages: {
[data.appState.currentPageId]: after,
},
},
},
}

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ export class DrawSession implements Session {
this.points = [[0, 0, 0.5]]
}
start = (data: Data) => data
start = () => void null
update = (data: Data, point: number[], pressure: number, isLocked = false) => {
const { snapshot } = this
@ -89,38 +89,42 @@ export class DrawSession implements Session {
if (this.points.length <= 2) return data
return {
page: {
...data.page,
shapes: {
...data.page.shapes,
[snapshot.id]: {
...data.page.shapes[snapshot.id],
points: [...this.points],
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[snapshot.id]: {
points: [...this.points],
},
},
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [snapshot.id],
},
},
},
pageState: {
...data.pageState,
selectedIds: [snapshot.id],
},
}
}
cancel = (data: Data): Data => {
cancel = (data: Data) => {
const { snapshot } = this
return {
page: {
...data.page,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
shapes: {
...data.page.shapes,
[snapshot.id]: undefined,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[snapshot.id]: undefined,
},
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [],
},
},
},
pageState: {
...data.pageState,
selectedIds: [],
},
}
}
@ -130,23 +134,35 @@ export class DrawSession implements Session {
return {
id: 'create_draw',
before: {
page: {
shapes: {
[snapshot.id]: undefined,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[snapshot.id]: undefined,
},
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [],
},
},
},
pageState: {
selectedIds: [],
},
},
after: {
page: {
shapes: {
[snapshot.id]: TLDR.onSessionComplete(data, data.page.shapes[snapshot.id]),
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[snapshot.id]: TLDR.onSessionComplete(data, TLDR.getShape(data, snapshot.id)),
},
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [],
},
},
},
pageState: {
selectedIds: [],
},
},
}
@ -155,7 +171,8 @@ export class DrawSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
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
return {

View file

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

View file

@ -1,7 +1,8 @@
import { Utils, Vec } from '@tldraw/core'
import { Session, TLDrawStatus } from '~types'
import { Session, TLDrawShape, TLDrawStatus } from '~types'
import type { Data } from '~types'
import { TLDR } from '~state/tldr'
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
const PI2 = Math.PI * 2
@ -18,22 +19,19 @@ export class RotateSession implements Session {
this.snapshot = getRotateSnapshot(data)
}
start = (data: Data) => data
start = () => void null
update = (data: Data, point: number[], isLocked = false) => {
const { commonBoundsCenter, initialShapes } = this.snapshot
const page = TLDR.getPage(data)
const pageState = TLDR.getPageState(data)
const next = {
page: {
...data.page,
},
pageState: {
...data.pageState,
},
const shapes: Record<string, TLDrawShape> = {}
for (const { id, shape } of initialShapes) {
shapes[id] = shape
}
const { page, pageState } = next
const a1 = Vec.angle(commonBoundsCenter, this.origin)
const a2 = Vec.angle(commonBoundsCenter, point)
@ -47,52 +45,47 @@ export class RotateSession implements Session {
pageState.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
next.page.shapes = {
...next.page.shapes,
...Object.fromEntries(
initialShapes.map(({ id, center, offset, shape: { rotation = 0 } }) => {
const shape = page.shapes[id]
initialShapes.forEach(({ id, center, offset, shape: { rotation = 0 } }) => {
const shape = page.shapes[id]
const nextRotation = isLocked
? Utils.clampToRotationToSegments(rotation + rot, 24)
: rotation + rot
const nextRotation = isLocked
? Utils.clampToRotationToSegments(rotation + rot, 24)
: rotation + rot
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset)
const nextPoint = Vec.sub(Vec.rotWith(center, commonBoundsCenter, rot), offset)
return [
id,
{
...next.page.shapes[id],
...TLDR.mutate(data, shape, {
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
}),
},
]
})
),
}
shapes[id] = TLDR.mutate(data, shape, {
point: nextPoint,
rotation: (PI2 + nextRotation) % PI2,
})
})
return {
page: next.page,
document: {
pages: {
[data.appState.currentPageId]: {
shapes,
},
},
},
}
}
cancel = (data: Data) => {
const { initialShapes } = this.snapshot
const shapes: Record<string, TLDrawShape> = {}
for (const { id, shape } of initialShapes) {
data.page.shapes[id] = { ...shape }
shapes[id] = shape
}
return {
page: {
...data.page,
shapes: {
...data.page.shapes,
...Object.fromEntries(
initialShapes.map(({ id, shape }) => [id, TLDR.onSessionComplete(data, shape)])
),
document: {
pages: {
[data.appState.currentPageId]: {
shapes,
},
},
},
}
@ -103,25 +96,33 @@ export class RotateSession implements Session {
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 {
id: 'rotate',
before: {
page: {
shapes: Object.fromEntries(
initialShapes.map(({ shape: { id, point, rotation = undefined } }) => {
return [id, { point, rotation }]
})
),
document: {
pages: {
[data.appState.currentPageId]: {
shapes: beforeShapes,
},
},
},
},
after: {
page: {
shapes: Object.fromEntries(
this.snapshot.initialShapes.map(({ shape }) => {
const { point, rotation } = data.page.shapes[shape.id]
return [shape.id, { point, rotation }]
})
),
document: {
pages: {
[data.appState.currentPageId]: {
shapes: afterShapes,
},
},
},
},
}
@ -130,6 +131,7 @@ export class RotateSession implements Session {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getRotateSnapshot(data: Data) {
const pageState = TLDR.getPageState(data)
const initialShapes = TLDR.getSelectedBranchSnapshot(data)
if (initialShapes.length === 0) {
@ -152,7 +154,7 @@ export function getRotateSnapshot(data: Data) {
return {
hasUnlockedShapes,
boundsRotation: data.pageState.boundsRotation || 0,
boundsRotation: pageState.boundsRotation || 0,
commonBoundsCenter,
initialShapes: initialShapes
.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 { Data } from '~types'
import { TLDR } from '~state/tldr'
@ -14,21 +14,23 @@ export class TextSession implements Session {
start = (data: Data) => {
return {
...data,
pageState: {
...data.pageState,
editingId: this.initialShape.id,
document: {
pageStates: {
[data.appState.currentPageId]: {
editingId: this.initialShape.id,
},
},
},
}
}
update = (data: Data, text: string): Data => {
update = (data: Data, text: string) => {
const {
initialShape: { id },
} = this
let nextShape: TextShape = {
...(data.page.shapes[id] as TextShape),
...TLDR.getShape<TextShape>(data, id),
text,
}
@ -38,12 +40,13 @@ export class TextSession implements Session {
} as TextShape
return {
...data,
page: {
...data.page,
shapes: {
...data.page.shapes,
[id]: nextShape,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[id]: nextShape,
},
},
},
},
}
@ -55,17 +58,19 @@ export class TextSession implements Session {
} = this
return {
...data,
page: {
...data.page,
shapes: {
...data.page.shapes,
[id]: TLDR.onSessionComplete(data, data.page.shapes[id]),
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[id]: TLDR.onSessionComplete(data, TLDR.getShape(data, id)),
},
},
},
pageState: {
[data.appState.currentPageId]: {
editingId: undefined,
},
},
},
pageState: {
...data.pageState,
editingId: undefined,
},
}
}
@ -73,30 +78,45 @@ export class TextSession implements Session {
complete(data: Data) {
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 {
id: 'text',
before: {
page: {
shapes: {
[initialShape.id]: initialShape,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[initialShape.id]: initialShape,
},
},
},
pageState: {
[data.appState.currentPageId]: {
editingId: undefined,
},
},
},
pageState: {
editingId: undefined,
},
},
after: {
page: {
shapes: {
[initialShape.id]: TLDR.onSessionComplete(data, data.page.shapes[initialShape.id]),
document: {
pages: {
[data.appState.currentPageId]: {
shapes: {
[initialShape.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, initialShape.id)
),
},
},
},
pageState: {
[data.appState.currentPageId]: {
editingId: undefined,
},
},
},
pageState: {
editingId: undefined,
},
},
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { Utils, Vec } from '@tldraw/core'
import { TLPageState, Utils, Vec } from '@tldraw/core'
import {
TLDrawShape,
TLDrawBinding,
@ -7,6 +7,7 @@ import {
Data,
Command,
TLDrawStatus,
ShapesWithProp,
} from '~types'
import { TLDR } from '~state/tldr'
@ -24,22 +25,22 @@ export class TranslateSession implements Session {
this.snapshot = getTranslateSnapshot(data)
}
start = (data: Data): Partial<Data> => {
start = (data: Data) => {
const { bindingsToDelete } = this.snapshot
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])
const nextShapes = { ...data.page.shapes }
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
return {
page: {
...data.page,
shapes: nextShapes,
bindings: nextBindings,
document: {
pages: {
[data.appState.currentPageId]: {
bindings: nextBindings,
},
},
},
}
}
@ -47,12 +48,9 @@ export class TranslateSession implements Session {
update = (data: Data, point: number[], isAligned = false, isCloning = false) => {
const { clones, initialShapes } = this.snapshot
const next = {
...data,
page: { ...data.page },
shapes: { ...data.page.shapes },
pageState: { ...data.pageState },
}
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
const nextPageState: Partial<TLPageState> = {}
const delta = Vec.sub(point, this.origin)
@ -77,105 +75,92 @@ export class TranslateSession implements Session {
// Move original shapes back to start
next.page.shapes = {
...next.page.shapes,
...Object.fromEntries(
initialShapes.map((shape) => [
shape.id,
{ ...next.page.shapes[shape.id], point: shape.point },
])
),
}
initialShapes.forEach((shape) => (nextShapes[shape.id] = { point: shape.point }))
next.page.shapes = {
...next.page.shapes,
...Object.fromEntries(
clones.map((clone) => [
clone.id,
{ ...clone, point: Vec.round(Vec.add(clone.point, delta)) },
])
),
}
clones.forEach(
(shape) =>
(nextShapes[shape.id] = { ...shape, point: Vec.round(Vec.add(shape.point, delta)) })
)
next.pageState.selectedIds = clones.map((c) => c.id)
nextPageState.selectedIds = clones.map((shape) => shape.id)
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
next.page.shapes = {
...next.page.shapes,
...Object.fromEntries(
clones.map((clone) => [
clone.id,
{
...clone,
point: Vec.round(Vec.add(next.page.shapes[clone.id].point, trueDelta)),
},
])
),
}
clones.forEach((shape) => {
const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id)
return { page: { ...next.page }, pageState: { ...next.pageState } }
}
if (!current.point) throw Error('No point on that clone!')
// If not cloning...
// Cloning -> Not Cloning
if (this.isCloning) {
this.isCloning = false
next.page.shapes = { ...next.page.shapes }
// Delete the clones
clones.forEach((clone) => delete next.page.shapes[clone.id])
// Move the original shapes back to the cursor position
initialShapes.forEach((shape) => {
next.page.shapes[shape.id] = {
...next.page.shapes[shape.id],
point: Vec.round(Vec.add(shape.point, delta)),
nextShapes[shape.id] = {
...nextShapes[shape.id],
point: Vec.round(Vec.add(current.point, trueDelta)),
}
})
} else {
// If not cloning...
// Delete the cloned bindings
next.page.bindings = { ...next.page.bindings }
// Cloning -> Not Cloning
if (this.isCloning) {
this.isCloning = false
for (const binding of this.snapshot.clonedBindings) {
delete next.page.bindings[binding.id]
// Delete the clones
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
// Move the original shapes back to the cursor position
initialShapes.forEach((shape) => {
nextShapes[shape.id] = {
point: Vec.round(Vec.add(shape.point, delta)),
}
})
// Delete the cloned bindings
for (const binding of this.snapshot.clonedBindings) {
nextBindings[binding.id] = undefined
}
// Set selected ids
nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
}
// Set selected ids
next.pageState.selectedIds = initialShapes.map((c) => c.id)
// Move the shapes by the delta
initialShapes.forEach((shape) => {
const current = nextShapes[shape.id] || TLDR.getShape(data, shape.id)
if (!current.point) throw Error('No point on that clone!')
nextShapes[shape.id] = {
...nextShapes[shape.id],
point: Vec.round(Vec.add(current.point, trueDelta)),
}
})
}
// Move the shapes by the delta
next.page.shapes = {
...next.page.shapes,
...Object.fromEntries(
initialShapes.map((shape) => [
shape.id,
{
...next.page.shapes[shape.id],
point: Vec.round(Vec.add(next.page.shapes[shape.id].point, trueDelta)),
return {
document: {
pages: {
[data.appState.currentPageId]: {
shapes: nextShapes,
bindings: nextBindings,
},
])
),
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
},
}
return { page: { ...next.page }, pageState: { ...next.pageState } }
}
cancel = (data: Data) => {
const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot
const nextShapes: Record<string, TLDrawShape> = { ...data.page.shapes }
const nextBindings = { ...data.page.bindings }
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
const nextPageState: Partial<TLPageState> = {}
// Put back any deleted bindings
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 }))
// Delete clones
clones.forEach((clone) => delete nextShapes[clone.id])
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
// Delete cloned bindings
clonedBindings.forEach((binding) => delete nextBindings[binding.id])
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
nextPageState.selectedIds = this.snapshot.selectedIds
return {
page: {
...data.page,
shapes: nextShapes,
bindings: nextBindings,
},
pageState: {
...data.pageState,
selectedIds: this.snapshot.selectedIds,
document: {
pages: {
[data.appState.currentPageId]: {
shapes: nextShapes,
bindings: nextBindings,
},
pageStates: {
[data.appState.currentPageId]: nextPageState,
},
},
},
}
}
@ -205,36 +194,33 @@ export class TranslateSession implements Session {
complete(data: Data): Command {
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot
const before: PagePartial = {
shapes: {
...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 beforeBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const beforeShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
const after: PagePartial = {
shapes: {
...Object.fromEntries(clones.map((clone) => [clone.id, data.page.shapes[clone.id]])),
...Object.fromEntries(
initialShapes.map((shape) => [shape.id, { point: data.page.shapes[shape.id].point }])
),
},
bindings: {
...Object.fromEntries(
clonedBindings.map((binding) => [binding.id, data.page.bindings[binding.id]])
),
...Object.fromEntries(bindingsToDelete.map((binding) => [binding.id, undefined])),
},
}
const afterBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const afterShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
clones.forEach((shape) => {
beforeShapes[shape.id] = undefined
afterShapes[shape.id] = this.isCloning ? TLDR.getShape(data, shape.id) : undefined
})
initialShapes.forEach((shape) => {
beforeShapes[shape.id] = { point: shape.point }
afterShapes[shape.id] = { point: TLDR.getShape(data, shape.id).point }
})
clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
afterBindings[binding.id] = TLDR.getBinding(data, binding.id)
})
bindingsToDelete.forEach((binding) => {
beforeBindings[binding.id] = binding
for (const id of [binding.toId, binding.fromId]) {
// 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 (!shape.handles) continue
@ -242,17 +228,26 @@ export class TranslateSession implements Session {
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 },
},
let shape: Partial<TLDrawShape> | undefined = beforeShapes[id]
if (!shape) shape = {}
TLDR.assertShapeHasProperty(shape as TLDrawShape, 'handles')
if (!beforeShapes[id]) {
beforeShapes[id] = { handles: {} }
}
after.shapes[id] = {
...after.shapes[id],
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
if (!afterShapes[id]) {
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 {
id: 'translate',
before: {
page: before,
pageState: {
selectedIds: [...this.snapshot.selectedIds],
document: {
pages: {
[data.appState.currentPageId]: {
shapes: beforeShapes,
bindings: beforeBindings,
},
},
pageStates: {
[data.appState.currentPageId]: {
selectedIds: [...this.snapshot.selectedIds],
},
},
},
},
after: {
page: after,
pageState: {
selectedIds: [...data.pageState.selectedIds],
document: {
pages: {
[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 page = TLDR.getPage(data)
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) => {
const shape = TLDR.getShape(data, id)
return {
@ -315,7 +330,7 @@ export function getTranslateSnapshot(data: Data) {
for (const handle of Object.values(shape.handles)) {
if (handle.bindingId) {
const binding = data.page.bindings[handle.bindingId]
const binding = page.bindings[handle.bindingId]
const cloneBinding = {
...binding,
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 type {
Data,
@ -8,6 +8,7 @@ import type {
TLDrawShape,
TLDrawShapeUtil,
TLDrawBinding,
TLDrawPage,
} from '~types'
export class TLDR {
@ -16,11 +17,13 @@ export class TLDR {
}
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[]) {
const { camera } = data.pageState
const camera = this.getCamera(data)
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
}
@ -42,32 +45,32 @@ export class TLDR {
return Utils.clamp(zoom, 0.1, 5)
}
static getCurrentCamera(data: Data) {
return data.pageState.camera
static getPage(data: Data, pageId = data.appState.currentPageId): TLDrawPage {
return data.document.pages[pageId]
}
static getPage(data: Data) {
return data.page
static getPageState(data: Data, pageId = data.appState.currentPageId): TLPageState {
return data.document.pageStates[pageId]
}
static getPageState(data: Data) {
return data.pageState
static getSelectedIds(data: Data, pageId = data.appState.currentPageId): string[] {
return this.getPageState(data, pageId).selectedIds
}
static getSelectedIds(data: Data) {
return data.pageState.selectedIds
static getShapes(data: Data, pageId = data.appState.currentPageId): TLDrawShape[] {
return Object.values(this.getPage(data, pageId).shapes)
}
static getShapes(data: Data) {
return Object.values(data.page.shapes)
static getCamera(data: Data, pageId = data.appState.currentPageId): TLPageState['camera'] {
return this.getPageState(data, pageId).camera
}
static getCamera(data: Data) {
return data.pageState.camera
}
static getShape<T extends TLDrawShape = TLDrawShape>(data: Data, shapeId: string): T {
return data.page.shapes[shapeId] as T
static getShape<T extends TLDrawShape = TLDrawShape>(
data: Data,
shapeId: string,
pageId = data.appState.currentPageId
): T {
return this.getPage(data, pageId).shapes[shapeId] as T
}
static getBounds<T extends TLDrawShape>(shape: T) {
@ -85,24 +88,26 @@ export class TLDR {
}
static getParentId(data: Data, id: string) {
const shape = data.page.shapes[id]
return shape.parentId
return this.getShape(data, id).parentId
}
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
return shape.parentId === data.pageState.currentParentId || shape.parentId === data.page.id
return shape.parentId === pageState.currentParentId || shape.parentId === page.id
? id
: this.getPointedId(data, shape.parentId)
}
static getDrilledPointedId(data: Data, id: string): string {
const shape = data.page.shapes[id]
const { currentParentId, pointedId } = data.pageState
const shape = this.getShape(data, id)
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 === currentParentId
? id
@ -110,20 +115,22 @@ export class TLDR {
}
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) {
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
: this.getTopParentId(data, shape.parentId)
}
// Get an array of a shape id and its descendant shapes' ids
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]
@ -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.
// Use this to decide which shapes to clone as before / after for a command.
static getAllEffectedShapeIds(data: Data, ids: string[]): string[] {
const page = this.getPage(data)
const visited = new Set(ids)
ids.forEach((id) => {
const shape = data.page.shapes[id]
const shape = page.shapes[id]
// Add descendant shapes
function collectDescendants(shape: TLDrawShape): void {
@ -190,7 +199,7 @@ export class TLDR {
.filter((childId) => !visited.has(childId))
.forEach((childId) => {
visited.add(childId)
collectDescendants(data.page.shapes[childId])
collectDescendants(page.shapes[childId])
})
}
@ -199,17 +208,17 @@ export class TLDR {
// Add asecendant shapes
function collectAscendants(shape: TLDrawShape): void {
const parentId = shape.parentId
if (parentId === data.page.id) return
if (parentId === page.id) return
if (visited.has(parentId)) return
visited.add(parentId)
collectAscendants(data.page.shapes[parentId])
collectAscendants(page.shapes[parentId])
}
collectAscendants(shape)
// Add bindings that are to or from any of the visited shapes (this does not have to be recursive)
visited.forEach((id) => {
Object.values(data.page.bindings)
Object.values(page.bindings)
.filter((binding) => binding.fromId === id || binding.toId === id)
.forEach((binding) => visited.add(binding.fromId === id ? binding.toId : binding.fromId))
})
@ -225,31 +234,29 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {}
): 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) {
const deltas = this.getShapeUtils(shape).updateChildren(
shape,
shape.children.map((childId) => data.page.shapes[childId])
shape.children.map((childId) => page.shapes[childId])
)
if (deltas) {
return deltas.reduce<Data>((cData, delta) => {
if (!delta.id) throw Error('Delta must include an id!')
const cPage = this.getPage(cData)
const deltaShape = this.getShape(cData, delta.id)
const deltaShape = cData.page.shapes[delta.id]
if (!beforeShapes[deltaShape.id]) {
beforeShapes[deltaShape.id] = deltaShape
if (!beforeShapes[delta.id]) {
beforeShapes[delta.id] = deltaShape
}
cData.page.shapes[deltaShape.id] = this.getShapeUtils(deltaShape).mutate(
deltaShape,
delta
)
afterShapes[deltaShape.id] = cData.page.shapes[deltaShape.id]
cPage.shapes[delta.id] = this.getShapeUtils(deltaShape).mutate(deltaShape, delta)
afterShapes[delta.id] = cPage.shapes[delta.id]
if (deltaShape.children !== undefined) {
this.recursivelyUpdateChildren(cData, deltaShape.id, beforeShapes, afterShapes)
this.recursivelyUpdateChildren(cData, delta.id, beforeShapes, afterShapes)
}
return cData
@ -266,32 +273,50 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {}
): 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) {
const parent = data.page.shapes[shape.parentId] as T
if (page.id === 'doc') {
throw Error('wtf')
}
if (shape.parentId !== page.id) {
const parent = this.getShape(data, shape.parentId)
if (!parent.children) throw Error('No children in parent!')
const delta = this.getShapeUtils(shape).onChildrenChange(
const delta = this.getShapeUtils(parent).onChildrenChange(
parent,
parent.children.map((childId) => data.page.shapes[childId])
parent.children.map((childId) => this.getShape(data, childId))
)
if (delta) {
if (!beforeShapes[parent.id]) {
beforeShapes[parent.id] = parent
}
data.page.shapes[parent.id] = this.getShapeUtils(parent).mutate(parent, delta)
afterShapes[parent.id] = data.page.shapes[parent.id]
page.shapes[parent.id] = this.getShapeUtils(parent).mutate(parent, delta)
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 data
if (data.appState.currentPageId === 'doc') {
console.error('WTF?')
}
return {
...data,
document: {
...data.document,
pages: {
...data.document.pages,
[page.id]: page,
},
},
}
}
static updateBindings(
@ -300,26 +325,27 @@ export class TLDR {
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
afterShapes: Record<string, Partial<TLDrawShape>> = {}
): 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)
.reduce((cData, binding) => {
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]) {
beforeShapes[binding.toId] = Utils.deepClone(cData.page.shapes[binding.toId])
beforeShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId))
}
this.onBindingChange(
cData,
cData.page.shapes[binding.fromId],
this.getShape(cData, binding.fromId),
binding,
cData.page.shapes[binding.toId]
this.getShape(cData, binding.toId)
)
afterShapes[binding.fromId] = Utils.deepClone(cData.page.shapes[binding.fromId])
afterShapes[binding.toId] = Utils.deepClone(cData.page.shapes[binding.toId])
afterShapes[binding.fromId] = Utils.deepClone(this.getShape(cData, binding.fromId))
afterShapes[binding.toId] = Utils.deepClone(this.getShape(cData, binding.toId))
return cData
}, data)
@ -357,34 +383,28 @@ export class TLDR {
/* Mutations */
/* -------------------------------------------------- */
static setSelectedIds(data: Data, ids: string[]) {
data.pageState.selectedIds = ids
}
static deselectAll(data: Data) {
this.setSelectedIds(data, [])
}
static mutateShapes<T extends TLDrawShape>(
data: Data,
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>>
after: Record<string, Partial<T>>
data: Data
} {
const page = { ...this.getPage(data, pageId) }
const beforeShapes: Record<string, Partial<T>> = {}
const afterShapes: Record<string, Partial<T>> = {}
ids.forEach((id, i) => {
const shape = this.getShape<T>(data, id)
const shape = this.getShape<T>(data, id, pageId)
const change = fn(shape, i)
beforeShapes[id] = Object.fromEntries(
Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T>
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) => {
@ -408,48 +428,61 @@ export class TLDR {
static createShapes(
data: Data,
shapes: TLDrawShape[]
shapes: TLDrawShape[],
pageId = data.appState.currentPageId
): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const page = this.getPage(data)
const before: DeepPartial<Data> = {
page: {
shapes: {
...Object.fromEntries(
shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, undefined]]
document: {
pages: {
[pageId]: {
shapes: {
...Object.fromEntries(
shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, undefined],
]
// If the shape is a child of another shape, also save that shape
if (shape.parentId !== data.page.id) {
const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: parent.children }])
}
// If the shape is a child of another shape, also save that shape
if (shape.parentId !== pageId) {
const parent = this.getShape(data, shape.parentId, pageId)
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: parent.children }])
}
return results
})
),
return results
})
),
},
},
},
},
}
const after: DeepPartial<Data> = {
page: {
shapes: {
...Object.fromEntries(
shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]]
document: {
pages: {
[pageId]: {
shapes: {
shapes: {
...Object.fromEntries(
shapes.flatMap((shape) => {
const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, shape],
]
// If the shape is a child of a different shape, update its parent
if (shape.parentId !== data.page.id) {
const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: [...parent.children, shape.id] }])
}
// If the shape is a child of a different shape, update its parent
if (shape.parentId !== pageId) {
const parent = this.getShape(data, shape.parentId, pageId)
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: [...parent.children, shape.id] }])
}
return results
})
),
return results
})
),
},
},
},
},
},
}
@ -462,9 +495,10 @@ export class TLDR {
static deleteShapes(
data: Data,
shapes: TLDrawShape[] | string[]
shapes: TLDrawShape[] | string[],
pageId = data.appState.currentPageId
): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const page = this.getPage(data)
const page = this.getPage(data, pageId)
const shapeIds =
typeof shapes[0] === 'string'
@ -472,63 +506,73 @@ export class TLDR {
: (shapes as TLDrawShape[]).map((shape) => shape.id)
const before: DeepPartial<Data> = {
page: {
shapes: {
// These are the shapes that we're going to delete
...Object.fromEntries(
shapeIds.flatMap((id) => {
const shape = page.shapes[id]
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]]
document: {
pages: {
[pageId]: {
shapes: {
// These are the shapes that we're going to delete
...Object.fromEntries(
shapeIds.flatMap((id) => {
const shape = page.shapes[id]
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]]
// If the shape is a child of another shape, also add that shape
if (shape.parentId !== data.page.id) {
const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: parent.children }])
}
// If the shape is a child of another shape, also add that shape
if (shape.parentId !== pageId) {
const parent = page.shapes[shape.parentId]
if (!parent.children) throw Error('No children in parent!')
results.push([parent.id, { children: parent.children }])
}
return results
})
),
},
bindings: {
// These are the bindings that we're going to delete
...Object.fromEntries(
Object.values(page.bindings)
.filter((binding) => {
return shapeIds.includes(binding.fromId) || shapeIds.includes(binding.toId)
})
.map((binding) => {
return [binding.id, binding]
})
),
return results
})
),
},
bindings: {
// These are the bindings that we're going to delete
...Object.fromEntries(
Object.values(page.bindings)
.filter((binding) => {
return shapeIds.includes(binding.fromId) || shapeIds.includes(binding.toId)
})
.map((binding) => {
return [binding.id, binding]
})
),
},
},
},
},
}
const after: DeepPartial<Data> = {
page: {
shapes: {
...Object.fromEntries(
shapeIds.flatMap((id) => {
const shape = page.shapes[id]
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, undefined]]
document: {
pages: {
[pageId]: {
shapes: {
...Object.fromEntries(
shapeIds.flatMap((id) => {
const shape = page.shapes[id]
const results: [string, Partial<TLDrawShape> | undefined][] = [
[shape.id, undefined],
]
// If the shape is a child of a different shape, update its parent
if (shape.parentId !== data.page.id) {
const parent = page.shapes[shape.parentId]
// If the shape is a child of a different shape, update its parent
if (shape.parentId !== page.id) {
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.filter((id) => id !== shape.id) },
])
}
results.push([
parent.id,
{ children: parent.children.filter((id) => id !== shape.id) },
])
}
return results
})
),
return results
})
),
},
},
},
},
}
@ -550,7 +594,7 @@ export class TLDR {
const delta = getShapeUtils(shape).onChildrenChange(
shape,
shape.children.map((id) => data.page.shapes[id])
shape.children.map((id) => this.getShape(data, id))
)
if (!delta) return shape
return this.mutate(data, shape, delta)
@ -598,7 +642,7 @@ export class TLDR {
next = this.onChildrenChange(data, next) || next
}
data.page.shapes[next.id] = next
// data.page.shapes[next.id] = next
return next
}
@ -608,13 +652,15 @@ export class TLDR {
/* -------------------------------------------------- */
static updateParents(data: Data, changedShapeIds: string[]): void {
const page = this.getPage(data)
if (changedShapeIds.length === 0) return
const { shapes } = this.getPage(data)
const parentToUpdateIds = Array.from(
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) {
const parent = shapes[parentId]
@ -631,18 +677,17 @@ export class TLDR {
static getSelectedStyle(data: Data): ShapeStyles | false {
const {
page,
pageState,
appState: { currentStyle },
} = data
const page = this.getPage(data)
const pageState = this.getPageState(data)
if (pageState.selectedIds.length === 0) {
return currentStyle
}
const shapeStyles = data.pageState.selectedIds.map((id) => {
return page.shapes[id].style
})
const shapeStyles = pageState.selectedIds.map((id) => page.shapes[id].style)
const commonStyle = {} as ShapeStyles
@ -673,17 +718,17 @@ export class TLDR {
/* Bindings */
/* -------------------------------------------------- */
static getBinding(data: Data, id: string): TLDrawBinding {
return this.getPage(data).bindings[id]
static getBinding(data: Data, id: string, pageId = data.appState.currentPageId): TLDrawBinding {
return this.getPage(data, pageId).bindings[id]
}
static getBindings(data: Data): TLDrawBinding[] {
const page = this.getPage(data)
static getBindings(data: Data, pageId = data.appState.currentPageId): TLDrawBinding[] {
const page = this.getPage(data, pageId)
return Object.values(page.bindings)
}
static getBindableShapeIds(data: Data) {
return Object.values(data.page.shapes)
return this.getShapes(data)
.filter((shape) => TLDR.getShapeUtils(shape).canBind)
.sort((a, b) => b.childIndex - a.childIndex)
.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[] {
const changedShapeIds = new Set(ids)
const page = this.getPage(data)
// 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
const bindingsToUpdate = new Set(
@ -751,42 +787,6 @@ export class TLDR {
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 */
/* -------------------------------------------------- */
@ -799,51 +799,4 @@ export class TLDR {
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
editingId?: string
pointedBoundsHandle?: TLBoundsCorner | TLBoundsEdge | 'rotate'
currentDocumentId = 'doc'
currentPageId = 'page'
document: TLDrawDocument
isCreating = false
@ -142,26 +141,29 @@ export class TLDrawState implements TLCallbacks {
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
if (result.document) {
for (const pageId in result.document.pages) {
const currentPage = next.document.pages[pageId]
Object.values(current.document.pages).forEach((currentPage) => {
const pageId = currentPage.id
const nextPage = {
...next.document,
shapes: { ...currentPage.shapes },
bindings: { ...currentPage.bindings },
...currentPage,
...result.document?.pages[pageId],
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]
}
})
for (const id in nextPage.bindings) {
Object.keys(nextPage.bindings).forEach((id) => {
if (!nextPage.bindings[id]) delete nextPage.bindings[id]
}
})
const changedShapeIds = Object.values(nextPage.shapes)
.filter((shape) => currentPage.shapes[shape.id] !== shape)
.map((shape) => shape.id)
next.document.pages[pageId] = nextPage
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR.getRelatedBindings(next, changedShapeIds)
@ -203,7 +205,7 @@ export class TLDrawState implements TLCallbacks {
}
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
}
@ -214,7 +216,7 @@ export class TLDrawState implements TLCallbacks {
next.document.pages[pageId] = nextPage
next.document.pageStates[pageId] = nextPageState
}
})
}
// 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 => {
return this.document.pages[pageId].shapes[id] as T
return TLDR.getShape<T>(this.data, id, pageId)
}
getPage = (id = this.currentPageId) => {
return this.document.pages[id]
getPage = (pageId = this.currentPageId) => {
return TLDR.getPage(this.data, pageId)
}
getShapes = (id = this.currentPageId) => {
return Object.values(this.getPage(id).shapes).sort((a, b) => a.childIndex - b.childIndex)
getShapes = (pageId = this.currentPageId) => {
return TLDR.getShapes(this.data, pageId)
}
getPageState = (id = this.currentPageId) => {
return this.document.pageStates[id]
getBindings = (pageId = this.currentPageId) => {
return TLDR.getBindings(this.data, pageId)
}
getPageState = (pageId = this.currentPageId) => {
return TLDR.getPageState(this.data, pageId)
}
getAppState = () => {
@ -265,7 +271,7 @@ export class TLDrawState implements TLCallbacks {
}
getPagePoint = (point: number[]) => {
const { camera } = this.getPageState()
const { camera } = this.pageState
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']) => {
this._onChange = onChange
this.currentDocumentId = document.id
this.document = Utils.deepClone(document)
this.currentPageId = Object.keys(document.pages)[0]
this.selectHistory.pointer = 0
@ -637,7 +642,12 @@ export class TLDrawState implements TLCallbacks {
startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) {
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}`)
return this
}
@ -645,7 +655,7 @@ export class TLDrawState implements TLCallbacks {
updateSession<T extends Session>(...args: ParametersExceptFirst<T['update']>) {
const { session } = 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}`)
return this
}
@ -696,7 +706,10 @@ export class TLDrawState implements TLCallbacks {
this.isCreating = false
this._onChange?.(this, `session:cancel_create:${session.id}`)
} 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}`)
}
@ -719,7 +732,10 @@ export class TLDrawState implements TLCallbacks {
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
if (this.isCreating) {
@ -2108,7 +2124,7 @@ export class TLDrawState implements TLCallbacks {
}
get bindings() {
return this.getPage().bindings
return this.getBindings()
}
get pageState() {

View file

@ -40,7 +40,10 @@ export interface Data {
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
? T
@ -69,10 +72,10 @@ export interface SelectHistory {
export interface Session {
id: string
status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
update: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
complete: (data: Readonly<Data>, ...args: any[]) => Partial<Data> | Command
cancel: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
start: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | void
update: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
complete: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | Command | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
}
export enum TLDrawStatus {

View file

@ -95,5 +95,5 @@ export default function Editor(): JSX.Element {
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 { openDB, DBSchema } from "idb"
import type { TLDrawDocument } from "@tldraw/tldraw"
import * as React from 'react'
import { openDB, DBSchema } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw'
interface TLDatabase extends DBSchema {
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
* 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) {
// 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)
// A function that other parts of the program can use to manually update
// the state to the latest value in the database.
const forceUpdate = React.useCallback(() => {
_setValue(null)
setStatus("loading")
setStatus('loading')
openDB<TLDatabase>("db", 1).then((db) =>
db.get("documents", id).then((v) => {
openDB<TLDatabase>('db', VERSION).then((db) =>
db.get('documents', id).then((v) => {
if (!v) throw Error(`Could not find document with id: ${id}`)
_setValue(v)
setStatus("ready")
setStatus('ready')
})
)
}, [id])
@ -46,7 +48,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// value in the database.
const setValue = React.useCallback(
(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]
)
@ -55,17 +57,17 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state.
React.useEffect(() => {
async function handleLoad() {
const db = await openDB<TLDatabase>("db", 1, {
const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) {
db.createObjectStore("documents")
db.createObjectStore('documents')
},
})
let savedDoc: TLDrawDocument
try {
const restoredDoc = await db.get("documents", id)
if (!restoredDoc) throw Error("No document")
const restoredDoc = await db.get('documents', id)
if (!restoredDoc) throw Error('No document')
savedDoc = restoredDoc
restoredDoc.pageStates = Object.fromEntries(
Object.entries(restoredDoc.pageStates).map(([pageId, pageState]) => [
@ -78,12 +80,12 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
])
)
} catch (e) {
await db.put("documents", doc, id)
await db.put('documents', doc, id)
savedDoc = doc
}
_setValue(savedDoc)
setStatus("ready")
setStatus('ready')
}
handleLoad()