Mostly fixed bugs
This commit is contained in:
parent
594bc7c2ff
commit
ad3db2c0ac
45 changed files with 1096 additions and 969 deletions
|
@ -102,5 +102,5 @@ export default function Editor(): JSX.Element {
|
|||
return <div />
|
||||
}
|
||||
|
||||
return <TLDraw document={value} onChange={handleChange} />
|
||||
return <TLDraw document={initialDoc} onChange={handleChange} />
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
})
|
||||
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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('')
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -95,5 +95,5 @@ export default function Editor(): JSX.Element {
|
|||
return <div />
|
||||
}
|
||||
|
||||
return <TLDraw document={value} onChange={handleChange} />
|
||||
return <TLDraw document={initialDoc} onChange={handleChange} />
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue