Adds move to page
This commit is contained in:
parent
dbed5f6bf4
commit
8ecddcbdbc
13 changed files with 657 additions and 199 deletions
|
@ -179,11 +179,11 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
|
||||||
<Kbd variant="menu">#⇧[</Kbd>
|
<Kbd variant="menu">#⇧[</Kbd>
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
</ContextMenuSubMenu>
|
</ContextMenuSubMenu>
|
||||||
|
<MoveToPageMenu />
|
||||||
{hasTwoOrMore && (
|
{hasTwoOrMore && (
|
||||||
<AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
|
<AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
|
||||||
)}
|
)}
|
||||||
<ContextMenuDivider />
|
<ContextMenuDivider />
|
||||||
{/* <MoveToPageMenu /> */}
|
|
||||||
<ContextMenuButton onSelect={handleCopy}>
|
<ContextMenuButton onSelect={handleCopy}>
|
||||||
<span>Copy</span>
|
<span>Copy</span>
|
||||||
<Kbd variant="menu">#C</Kbd>
|
<Kbd variant="menu">#C</Kbd>
|
||||||
|
@ -345,38 +345,42 @@ const StyledGrid = styled(MenuContent, {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// function MoveToPageMenu() {
|
const currentPageIdSelector = (s: Data) => s.appState.currentPageId
|
||||||
// const documentPages = useSelector((s) => s.data.document.pages)
|
const documentPagesSelector = (s: Data) => s.document.pages
|
||||||
// const currentPageId = useSelector((s) => s.data.currentPageId)
|
|
||||||
|
|
||||||
// if (!documentPages[currentPageId]) return null
|
function MoveToPageMenu(): JSX.Element | null {
|
||||||
|
const { tlstate, useSelector } = useTLDrawContext()
|
||||||
|
const currentPageId = useSelector(currentPageIdSelector)
|
||||||
|
const documentPages = useSelector(documentPagesSelector)
|
||||||
|
|
||||||
// const sorted = Object.values(documentPages)
|
const sorted = Object.values(documentPages)
|
||||||
// .sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||||
// .filter((a) => a.id !== currentPageId)
|
.filter((a) => a.id !== currentPageId)
|
||||||
|
|
||||||
// if (sorted.length === 0) return null
|
if (sorted.length === 0) return null
|
||||||
|
|
||||||
// return (
|
console.log(sorted)
|
||||||
// <ContextMenuRoot>
|
|
||||||
// <ContextMenuButton>
|
return (
|
||||||
// <span>Move To Page</span>
|
<ContextMenuRoot>
|
||||||
// <IconWrapper size="small">
|
<RadixContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
|
||||||
// <ChevronRightIcon />
|
<span>Move To Page</span>
|
||||||
// </IconWrapper>
|
<IconWrapper size="small">
|
||||||
// </ContextMenuButton>
|
<ChevronRightIcon />
|
||||||
// <MenuContent as={RadixContextMenu.Content} sideOffset={2} alignOffset={-2}>
|
</IconWrapper>
|
||||||
// {sorted.map(({ id, name }) => (
|
</RadixContextMenu.TriggerItem>
|
||||||
// <ContextMenuButton
|
<MenuContent as={RadixContextMenu.Content} sideOffset={2} alignOffset={-2}>
|
||||||
// key={id}
|
{sorted.map(({ id, name }, i) => (
|
||||||
// disabled={id === currentPageId}
|
<ContextMenuButton
|
||||||
// onSelect={() => state.send('MOVED_TO_PAGE', { id })}
|
key={id}
|
||||||
// >
|
disabled={id === currentPageId}
|
||||||
// <span>{name}</span>
|
onSelect={() => tlstate.moveToPage(id)}
|
||||||
// </ContextMenuButton>
|
>
|
||||||
// ))}
|
<span>{name || `Page ${i}`}</span>
|
||||||
// <ContextMenuArrow offset={13} />
|
</ContextMenuButton>
|
||||||
// </MenuContent>
|
))}
|
||||||
// </ContextMenuRoot>
|
<ContextMenuArrow offset={13} />
|
||||||
// )
|
</MenuContent>
|
||||||
// }
|
</ContextMenuRoot>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { TLDrawShape, Data, TLDrawCommand } from '~types'
|
||||||
|
|
||||||
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
|
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
|
|
||||||
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
||||||
const afterShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
const afterShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
||||||
|
|
||||||
|
|
|
@ -90,9 +90,26 @@ describe('Delete command', () => {
|
||||||
it('updates the group', () => {
|
it('updates the group', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.group(['rect1', 'rect2'], 'newGroup')
|
.group(['rect1', 'rect2', 'rect3'], 'newGroup')
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.delete()
|
.delete()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when deleting shapes with children', () => {
|
||||||
|
it('also deletes the children', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.group(['rect1', 'rect2'], 'newGroup')
|
||||||
|
.select('newGroup')
|
||||||
|
.delete()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('newGroup')).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import type { Data, TLDrawCommand, PagePartial, TLDrawShape, GroupShape } from '~types'
|
import type { Data, TLDrawCommand } from '~types'
|
||||||
|
import { removeShapesFromPage } from '../utils/removeShapesFromPage'
|
||||||
|
|
||||||
// - [ ] Update parents and possibly delete parents
|
// - [ ] Update parents and possibly delete parents
|
||||||
|
|
||||||
|
@ -8,82 +9,7 @@ export function deleteShapes(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
pageId = data.appState.currentPageId
|
pageId = data.appState.currentPageId
|
||||||
): TLDrawCommand {
|
): TLDrawCommand {
|
||||||
const before: PagePartial = {
|
const { before, after } = removeShapesFromPage(data, ids, pageId)
|
||||||
shapes: {},
|
|
||||||
bindings: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const after: PagePartial = {
|
|
||||||
shapes: {},
|
|
||||||
bindings: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentsToUpdate: GroupShape[] = []
|
|
||||||
|
|
||||||
const deletedIds = [...ids]
|
|
||||||
|
|
||||||
// These are the shapes we're definitely going to delete
|
|
||||||
|
|
||||||
ids.forEach((id) => {
|
|
||||||
const shape = TLDR.getShape(data, id, pageId)
|
|
||||||
before.shapes[id] = shape
|
|
||||||
after.shapes[id] = undefined
|
|
||||||
|
|
||||||
if (shape.parentId !== pageId) {
|
|
||||||
parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
parentsToUpdate.forEach((parent) => {
|
|
||||||
if (ids.includes(parent.id)) return
|
|
||||||
deletedIds.push(parent.id)
|
|
||||||
before.shapes[parent.id] = { children: parent.children }
|
|
||||||
after.shapes[parent.id] = { children: parent.children.filter((id) => !ids.includes(id)) }
|
|
||||||
})
|
|
||||||
|
|
||||||
// Recursively check for empty parents?
|
|
||||||
|
|
||||||
const page = TLDR.getPage(data, pageId)
|
|
||||||
|
|
||||||
// We also need to delete bindings that reference the deleted shapes
|
|
||||||
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) {
|
|
||||||
// Delete this binding
|
|
||||||
before.bindings[binding.id] = binding
|
|
||||||
after.bindings[binding.id] = undefined
|
|
||||||
|
|
||||||
// Let's also look each the bound shape...
|
|
||||||
const shape = TLDR.getShape(data, id, pageId)
|
|
||||||
|
|
||||||
// If the bound shape has a handle that references the deleted binding...
|
|
||||||
if (shape.handles) {
|
|
||||||
Object.values(shape.handles)
|
|
||||||
.filter((handle) => handle.bindingId === binding.id)
|
|
||||||
.forEach((handle) => {
|
|
||||||
// Save the binding reference in the before patch
|
|
||||||
before.shapes[id] = {
|
|
||||||
...before.shapes[id],
|
|
||||||
handles: {
|
|
||||||
...before.shapes[id]?.handles,
|
|
||||||
[handle.id]: { bindingId: binding.id },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unless we're currently deleting the shape, remove the
|
|
||||||
// binding reference from the after patch
|
|
||||||
if (!deletedIds.includes(id)) {
|
|
||||||
after.shapes[id] = {
|
|
||||||
...after.shapes[id],
|
|
||||||
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'delete_shapes',
|
id: 'delete_shapes',
|
||||||
|
|
|
@ -8,6 +8,7 @@ export * from './distribute'
|
||||||
export * from './duplicate-page'
|
export * from './duplicate-page'
|
||||||
export * from './duplicate'
|
export * from './duplicate'
|
||||||
export * from './flip'
|
export * from './flip'
|
||||||
|
export * from './group'
|
||||||
export * from './move'
|
export * from './move'
|
||||||
export * from './rename-page'
|
export * from './rename-page'
|
||||||
export * from './rotate'
|
export * from './rotate'
|
||||||
|
@ -17,4 +18,4 @@ export * from './toggle-decoration'
|
||||||
export * from './toggle'
|
export * from './toggle'
|
||||||
export * from './translate'
|
export * from './translate'
|
||||||
export * from './update'
|
export * from './update'
|
||||||
export * from './group'
|
export * from './move-to-page'
|
||||||
|
|
1
packages/tldraw/src/state/command/move-to-page/index.ts
Normal file
1
packages/tldraw/src/state/command/move-to-page/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './move-to-page.command'
|
|
@ -0,0 +1,250 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { mockDocument } from '~test'
|
||||||
|
import { ArrowShape, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
|
describe('Move to page command', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Moving shapes to a new page should remove those shapes from the
|
||||||
|
current page and add them to the specifed page. If bindings exist
|
||||||
|
that effect the moved shapes, then the bindings should be destroyed
|
||||||
|
on the old page and created on the new page only if both the "to"
|
||||||
|
and "from" shapes were moved. The app should then change pages to
|
||||||
|
the new page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
it('does, undoes and redoes command', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.createPage('page2')
|
||||||
|
.changePage('page1')
|
||||||
|
.select('rect1', 'rect2')
|
||||||
|
.moveToPage('page2')
|
||||||
|
|
||||||
|
expect(tlstate.currentPageId).toBe('page2')
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||||
|
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||||
|
expect(tlstate.currentPageId).toBe('page1')
|
||||||
|
|
||||||
|
tlstate.redo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||||
|
expect(tlstate.currentPageId).toBe('page2')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when moving shapes with bindings', () => {
|
||||||
|
it('deletes bindings when only the bound-to shape is moved', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.selectAll()
|
||||||
|
.delete()
|
||||||
|
.createShapes(
|
||||||
|
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||||
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
|
)
|
||||||
|
.select('arrow1')
|
||||||
|
.startHandleSession([200, 200], 'start')
|
||||||
|
.updateHandleSession([50, 50])
|
||||||
|
.completeSession()
|
||||||
|
|
||||||
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||||
|
|
||||||
|
tlstate.createPage('page2').changePage('page1').select('target1').moveToPage('page2')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||||
|
bindingId
|
||||||
|
)
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
|
||||||
|
tlstate.redo()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes bindings when only the bound-from shape is moved', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.selectAll()
|
||||||
|
.delete()
|
||||||
|
.createShapes(
|
||||||
|
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||||
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
|
)
|
||||||
|
.select('arrow1')
|
||||||
|
.startHandleSession([200, 200], 'start')
|
||||||
|
.updateHandleSession([50, 50])
|
||||||
|
.completeSession()
|
||||||
|
|
||||||
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||||
|
|
||||||
|
tlstate.createPage('page2').changePage('page1').select('arrow1').moveToPage('page2')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||||
|
bindingId
|
||||||
|
)
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
|
||||||
|
tlstate.redo()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves bindings when both shapes are moved', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.selectAll()
|
||||||
|
.delete()
|
||||||
|
.createShapes(
|
||||||
|
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||||
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
|
)
|
||||||
|
.select('arrow1')
|
||||||
|
.startHandleSession([200, 200], 'start')
|
||||||
|
.updateHandleSession([50, 50])
|
||||||
|
.completeSession()
|
||||||
|
|
||||||
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
|
||||||
|
|
||||||
|
tlstate
|
||||||
|
.createPage('page2')
|
||||||
|
.changePage('page1')
|
||||||
|
.select('arrow1', 'target1')
|
||||||
|
.moveToPage('page2')
|
||||||
|
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||||
|
bindingId
|
||||||
|
)
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||||
|
bindingId
|
||||||
|
)
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||||
|
|
||||||
|
tlstate.redo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||||
|
bindingId
|
||||||
|
)
|
||||||
|
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||||
|
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when moving grouped shapes', () => {
|
||||||
|
it('moves groups and their children', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.createPage('page2')
|
||||||
|
.changePage('page1')
|
||||||
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
|
.moveToPage('page2')
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page2')).toBeUndefined()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page1')).toBeDefined()
|
||||||
|
|
||||||
|
tlstate.redo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes groups shapes if the groups children were all moved', () => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reparents grouped shapes if the group is not moved', () => {
|
||||||
|
tlstate
|
||||||
|
.loadDocument(mockDocument)
|
||||||
|
.createPage('page2')
|
||||||
|
.changePage('page1')
|
||||||
|
.group(['rect1', 'rect2', 'rect3'], 'groupA')
|
||||||
|
.select('rect1')
|
||||||
|
.moveToPage('page2')
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page2').parentId).toBe('page2')
|
||||||
|
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual(['rect2', 'rect3'])
|
||||||
|
|
||||||
|
tlstate.undo()
|
||||||
|
|
||||||
|
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||||
|
expect(tlstate.getShape('rect1', 'page1').parentId).toBe('groupA')
|
||||||
|
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual([
|
||||||
|
'rect1',
|
||||||
|
'rect2',
|
||||||
|
'rect3',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,211 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import type { ArrowShape, Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||||
|
import { TLDR } from '~state/tldr'
|
||||||
|
import { Utils, Vec } from '@tldraw/core'
|
||||||
|
|
||||||
|
export function moveToPage(
|
||||||
|
data: Data,
|
||||||
|
ids: string[],
|
||||||
|
fromPageId: string,
|
||||||
|
toPageId: string
|
||||||
|
): TLDrawCommand {
|
||||||
|
const { currentPageId } = data.appState
|
||||||
|
|
||||||
|
const page = TLDR.getPage(data, currentPageId)
|
||||||
|
|
||||||
|
const fromPage: Record<string, PagePartial> = {
|
||||||
|
before: {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const toPage: Record<string, PagePartial> = {
|
||||||
|
before: {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all the shapes to move and their keys.
|
||||||
|
const movingShapeIds = new Set<string>()
|
||||||
|
const shapesToMove = new Set<TLDrawShape>()
|
||||||
|
|
||||||
|
ids
|
||||||
|
.map((id) => TLDR.getShape(data, id, fromPageId))
|
||||||
|
.forEach((shape) => {
|
||||||
|
movingShapeIds.add(shape.id)
|
||||||
|
shapesToMove.add(shape)
|
||||||
|
if (shape.children !== undefined) {
|
||||||
|
shape.children.forEach((childId) => {
|
||||||
|
movingShapeIds.add(childId)
|
||||||
|
shapesToMove.add(TLDR.getShape(data, childId, fromPageId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Where should we put start putting shapes on the "to" page?
|
||||||
|
const startingChildIndex = TLDR.getTopChildIndex(data, toPageId)
|
||||||
|
|
||||||
|
// Which shapes are we moving?
|
||||||
|
const movingShapes = Array.from(shapesToMove.values())
|
||||||
|
|
||||||
|
movingShapes.forEach((shape, i) => {
|
||||||
|
// Remove the shape from the fromPage
|
||||||
|
fromPage.before.shapes[shape.id] = shape
|
||||||
|
fromPage.after.shapes[shape.id] = undefined
|
||||||
|
|
||||||
|
// But the moved shape on the "to" page
|
||||||
|
toPage.before.shapes[shape.id] = undefined
|
||||||
|
toPage.after.shapes[shape.id] = shape
|
||||||
|
|
||||||
|
// If the shape's parent isn't moving too, reparent the shape to
|
||||||
|
// the "to" page, at the top of the z stack
|
||||||
|
if (!movingShapeIds.has(shape.parentId)) {
|
||||||
|
toPage.after.shapes[shape.id] = {
|
||||||
|
...shape,
|
||||||
|
parentId: toPageId,
|
||||||
|
childIndex: startingChildIndex + i,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the shape was in a group, then pull the shape from the
|
||||||
|
// parent's children array.
|
||||||
|
if (shape.parentId !== fromPageId) {
|
||||||
|
const parent = TLDR.getShape(data, shape.parentId, fromPageId)
|
||||||
|
fromPage.before.shapes[parent.id] = {
|
||||||
|
children: parent.children,
|
||||||
|
}
|
||||||
|
|
||||||
|
fromPage.after.shapes[parent.id] = {
|
||||||
|
children: parent.children!.filter((childId) => childId !== shape.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle bindings that effect duplicated shapes
|
||||||
|
Object.values(page.bindings)
|
||||||
|
.filter((binding) => movingShapeIds.has(binding.fromId) || movingShapeIds.has(binding.toId))
|
||||||
|
.forEach((binding) => {
|
||||||
|
// Always delete the binding from the from page
|
||||||
|
|
||||||
|
fromPage.before.bindings[binding.id] = binding
|
||||||
|
fromPage.after.bindings[binding.id] = undefined
|
||||||
|
|
||||||
|
// Delete the reference from the binding's fromShape
|
||||||
|
|
||||||
|
const fromBoundShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||||
|
|
||||||
|
// Will we be copying this binding to the new page?
|
||||||
|
|
||||||
|
const shouldCopy = movingShapeIds.has(binding.fromId) && movingShapeIds.has(binding.toId)
|
||||||
|
|
||||||
|
if (shouldCopy) {
|
||||||
|
// Just move the binding to the new page
|
||||||
|
toPage.before.bindings[binding.id] = undefined
|
||||||
|
toPage.after.bindings[binding.id] = binding
|
||||||
|
} else {
|
||||||
|
if (movingShapeIds.has(binding.fromId)) {
|
||||||
|
// If we are only moving the "from" shape, we need to delete
|
||||||
|
// the binding reference from the "from" shapes handles
|
||||||
|
const fromShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||||
|
const handle = Object.values(fromBoundShape.handles!).find(
|
||||||
|
(handle) => handle.bindingId === binding.id
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Remove the handle from the shape on the toPage
|
||||||
|
|
||||||
|
const handleId = handle.id as keyof ArrowShape['handles']
|
||||||
|
|
||||||
|
const toPageShape = toPage.after.shapes[fromShape.id]!
|
||||||
|
|
||||||
|
toPageShape.handles = {
|
||||||
|
...toPageShape.handles,
|
||||||
|
[handleId]: {
|
||||||
|
...toPageShape.handles![handleId],
|
||||||
|
bindingId: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we are only moving the "to" shape, we need to delete
|
||||||
|
// the binding reference from the "from" shape's handles
|
||||||
|
const fromShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||||
|
const handle = Object.values(fromBoundShape.handles!).find(
|
||||||
|
(handle) => handle.bindingId === binding.id
|
||||||
|
)!
|
||||||
|
|
||||||
|
fromPage.before.shapes[fromShape.id] = {
|
||||||
|
handles: { [handle.id]: { bindingId: binding.id } },
|
||||||
|
}
|
||||||
|
|
||||||
|
fromPage.after.shapes[fromShape.id] = {
|
||||||
|
handles: { [handle.id]: { bindingId: undefined } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Finally, center camera on selection
|
||||||
|
|
||||||
|
const toPageState = data.document.pageStates[toPageId]
|
||||||
|
const bounds = Utils.getCommonBounds(movingShapes.map((shape) => TLDR.getBounds(shape)))
|
||||||
|
const zoom = TLDR.getCameraZoom(
|
||||||
|
window.innerWidth < window.innerHeight
|
||||||
|
? (window.innerWidth - 128) / bounds.width
|
||||||
|
: (window.innerHeight - 128) / bounds.height
|
||||||
|
)
|
||||||
|
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
|
||||||
|
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
|
||||||
|
const point = Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'move_to_page',
|
||||||
|
before: {
|
||||||
|
appState: {
|
||||||
|
currentPageId: fromPageId,
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
[fromPageId]: fromPage.before,
|
||||||
|
[toPageId]: toPage.before,
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
[fromPageId]: { selectedIds: ids },
|
||||||
|
[toPageId]: {
|
||||||
|
selectedIds: toPageState.selectedIds,
|
||||||
|
camera: toPageState.camera,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
appState: {
|
||||||
|
currentPageId: toPageId,
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
[fromPageId]: fromPage.after,
|
||||||
|
[toPageId]: toPage.after,
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
[fromPageId]: { selectedIds: [] },
|
||||||
|
[toPageId]: {
|
||||||
|
selectedIds: ids,
|
||||||
|
camera: {
|
||||||
|
zoom,
|
||||||
|
point,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { TLDR } from '~state/tldr'
|
||||||
|
import type { Data, GroupShape, PagePartial } from '~types'
|
||||||
|
|
||||||
|
export function removeShapesFromPage(data: Data, ids: string[], pageId: string) {
|
||||||
|
const before: PagePartial = {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const after: PagePartial = {
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentsToUpdate: GroupShape[] = []
|
||||||
|
|
||||||
|
const deletedIds = new Set()
|
||||||
|
|
||||||
|
// These are the shapes we're definitely going to delete
|
||||||
|
|
||||||
|
ids.forEach((id) => {
|
||||||
|
deletedIds.add(id)
|
||||||
|
const shape = TLDR.getShape(data, id, pageId)
|
||||||
|
before.shapes[id] = shape
|
||||||
|
after.shapes[id] = undefined
|
||||||
|
|
||||||
|
// Also delete the shape's children
|
||||||
|
|
||||||
|
if (shape.children !== undefined) {
|
||||||
|
shape.children.forEach((childId) => {
|
||||||
|
deletedIds.add(childId)
|
||||||
|
const child = TLDR.getShape(data, childId, pageId)
|
||||||
|
before.shapes[childId] = child
|
||||||
|
after.shapes[childId] = undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.parentId !== pageId) {
|
||||||
|
parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
parentsToUpdate.forEach((parent) => {
|
||||||
|
if (ids.includes(parent.id)) return
|
||||||
|
deletedIds.add(parent.id)
|
||||||
|
before.shapes[parent.id] = { children: parent.children }
|
||||||
|
after.shapes[parent.id] = { children: parent.children.filter((id) => !ids.includes(id)) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Recursively check for empty parents?
|
||||||
|
|
||||||
|
const page = TLDR.getPage(data, pageId)
|
||||||
|
|
||||||
|
// We also need to delete bindings that reference the deleted shapes
|
||||||
|
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) {
|
||||||
|
// Delete this binding
|
||||||
|
before.bindings[binding.id] = binding
|
||||||
|
after.bindings[binding.id] = undefined
|
||||||
|
|
||||||
|
// Let's also look each the bound shape...
|
||||||
|
const shape = TLDR.getShape(data, id, pageId)
|
||||||
|
|
||||||
|
// If the bound shape has a handle that references the deleted binding...
|
||||||
|
if (shape.handles) {
|
||||||
|
Object.values(shape.handles)
|
||||||
|
.filter((handle) => handle.bindingId === binding.id)
|
||||||
|
.forEach((handle) => {
|
||||||
|
// Save the binding reference in the before patch
|
||||||
|
before.shapes[id] = {
|
||||||
|
...before.shapes[id],
|
||||||
|
handles: {
|
||||||
|
...before.shapes[id]?.handles,
|
||||||
|
[handle.id]: { bindingId: binding.id },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unless we're currently deleting the shape, remove the
|
||||||
|
// binding reference from the after patch
|
||||||
|
if (!deletedIds.has(id)) {
|
||||||
|
after.shapes[id] = {
|
||||||
|
...after.shapes[id],
|
||||||
|
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { before, after }
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDR } from '~state/tldr'
|
import { ArrowShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||||
import { ArrowShape, TLDrawShape, TLDrawStatus } from '~types'
|
|
||||||
|
|
||||||
describe('Arrow session', () => {
|
describe('Arrow session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -9,20 +8,9 @@ describe('Arrow session', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.delete()
|
.delete()
|
||||||
.create(
|
.createShapes(
|
||||||
TLDR.getShapeUtils({ type: 'rectangle' } as TLDrawShape).create({
|
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||||
id: 'target1',
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
parentId: 'page1',
|
|
||||||
point: [0, 0],
|
|
||||||
size: [100, 100],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.create(
|
|
||||||
TLDR.getShapeUtils({ type: 'arrow' } as TLDrawShape).create({
|
|
||||||
id: 'arrow1',
|
|
||||||
parentId: 'page1',
|
|
||||||
point: [200, 200],
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const restoreDoc = tlstate.document
|
const restoreDoc = tlstate.document
|
||||||
|
|
|
@ -317,71 +317,3 @@ export class ArrowSession implements Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findBinding(
|
|
||||||
data: Data,
|
|
||||||
shape: ArrowShape,
|
|
||||||
handle: TLHandle,
|
|
||||||
newBindingId: string,
|
|
||||||
bindableShapeIds: string[],
|
|
||||||
bindings: Record<string, TLDrawBinding | undefined>,
|
|
||||||
altKey: boolean,
|
|
||||||
metaKey: boolean
|
|
||||||
) {
|
|
||||||
let nextBinding: ArrowBinding | undefined = undefined
|
|
||||||
let nextTarget: TLDrawShape | undefined = undefined
|
|
||||||
|
|
||||||
if (!altKey) {
|
|
||||||
const oppositeHandle = shape.handles[handle.id === 'start' ? 'end' : 'start']
|
|
||||||
|
|
||||||
// Find the origin and direction of the handle
|
|
||||||
const rayOrigin = Vec.add(oppositeHandle.point, shape.point)
|
|
||||||
const rayPoint = Vec.add(handle.point, shape.point)
|
|
||||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
|
||||||
|
|
||||||
const oppositeBinding = oppositeHandle.bindingId
|
|
||||||
? bindings[oppositeHandle.bindingId]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// From all bindable shapes on the page...
|
|
||||||
for (const id of bindableShapeIds) {
|
|
||||||
if (id === shape.id) continue
|
|
||||||
if (id === shape.parentId) continue
|
|
||||||
if (id === oppositeBinding?.toId) continue
|
|
||||||
|
|
||||||
const target = TLDR.getShape(data, id, data.appState.currentPageId)
|
|
||||||
|
|
||||||
const util = TLDR.getShapeUtils(target)
|
|
||||||
|
|
||||||
const bindingPoint = util.getBindingPoint(
|
|
||||||
target,
|
|
||||||
shape,
|
|
||||||
rayPoint,
|
|
||||||
rayOrigin,
|
|
||||||
rayDirection,
|
|
||||||
32,
|
|
||||||
metaKey
|
|
||||||
)
|
|
||||||
|
|
||||||
// Not all shapes will produce a binding point
|
|
||||||
if (!bindingPoint) continue
|
|
||||||
|
|
||||||
// Stop at the first shape that will produce a binding point
|
|
||||||
nextTarget = target
|
|
||||||
|
|
||||||
nextBinding = {
|
|
||||||
id: newBindingId,
|
|
||||||
type: 'arrow',
|
|
||||||
fromId: shape.id,
|
|
||||||
handleId: handle.id as keyof ArrowShape['handles'],
|
|
||||||
toId: target.id,
|
|
||||||
point: Vec.round(bindingPoint.point),
|
|
||||||
distance: bindingPoint.distance,
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { target: nextTarget, binding: nextBinding }
|
|
||||||
}
|
|
||||||
|
|
|
@ -871,6 +871,15 @@ export class TLDR {
|
||||||
.reduce<TLDrawShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
.reduce<TLDrawShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTopChildIndex = (data: Data, pageId: string): number => {
|
||||||
|
const shapes = TLDR.getShapes(data, pageId)
|
||||||
|
return shapes.length === 0
|
||||||
|
? 1
|
||||||
|
: shapes
|
||||||
|
.filter((shape) => shape.parentId === pageId)
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Assertions */
|
/* Assertions */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -996,7 +996,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
||||||
|
|
||||||
const zoom = TLDR.getCameraZoom(
|
const zoom = TLDR.getCameraZoom(
|
||||||
bounds.width > bounds.height
|
window.innerWidth < window.innerHeight
|
||||||
? (window.innerWidth - 128) / bounds.width
|
? (window.innerWidth - 128) / bounds.width
|
||||||
: (window.innerHeight - 128) / bounds.height
|
: (window.innerHeight - 128) / bounds.height
|
||||||
)
|
)
|
||||||
|
@ -1016,12 +1016,12 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* @returns this
|
* @returns this
|
||||||
*/
|
*/
|
||||||
zoomToSelection = (): this => {
|
zoomToSelection = (): this => {
|
||||||
if (this.pageState.selectedIds.length === 0) return this
|
if (this.selectedIds.length === 0) return this
|
||||||
|
|
||||||
const bounds = TLDR.getSelectedBounds(this.state)
|
const bounds = TLDR.getSelectedBounds(this.state)
|
||||||
|
|
||||||
const zoom = TLDR.getCameraZoom(
|
const zoom = TLDR.getCameraZoom(
|
||||||
bounds.width > bounds.height
|
window.innerWidth < window.innerHeight
|
||||||
? (window.innerWidth - 128) / bounds.width
|
? (window.innerWidth - 128) / bounds.width
|
||||||
: (window.innerHeight - 128) / bounds.height
|
: (window.innerHeight - 128) / bounds.height
|
||||||
)
|
)
|
||||||
|
@ -1031,7 +1031,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
|
|
||||||
return this.setCamera(
|
return this.setCamera(
|
||||||
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
||||||
this.pageState.camera.zoom,
|
zoom,
|
||||||
`zoomed_to_selection`
|
`zoomed_to_selection`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1157,11 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* @returns this
|
* @returns this
|
||||||
*/
|
*/
|
||||||
select = (...ids: string[]): this => {
|
select = (...ids: string[]): this => {
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (!this.page.shapes[id]) {
|
||||||
|
throw Error(`That shape does not exist on page ${this.currentPageId}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
this.setSelectedIds(ids)
|
this.setSelectedIds(ids)
|
||||||
this.addToSelectHistory(ids)
|
this.addToSelectHistory(ids)
|
||||||
return this
|
return this
|
||||||
|
@ -1644,6 +1649,24 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
return this.setState(Commands.flip(this.state, ids, FlipType.Vertical))
|
return this.setState(Commands.flip(this.state, ids, FlipType.Vertical))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move one or more shapes to a new page. Will also break or move bindings.
|
||||||
|
* @param toPage The id of the page to move the shapes to.
|
||||||
|
* @param fromPage The id of the page to move the shapes from
|
||||||
|
*(defaults to current page).
|
||||||
|
* @param ids The ids of the shapes to move (defaults to selection).
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
moveToPage = (
|
||||||
|
toPageId: string,
|
||||||
|
fromPageId = this.currentPageId,
|
||||||
|
ids = this.selectedIds
|
||||||
|
): this => {
|
||||||
|
if (ids.length === 0) return this
|
||||||
|
this.setState(Commands.moveToPage(this.state, ids, fromPageId, toPageId))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move one or more shapes to the back of the page.
|
* Move one or more shapes to the back of the page.
|
||||||
* @param ids The ids of the shapes to change (defaults to selection).
|
* @param ids The ids of the shapes to change (defaults to selection).
|
||||||
|
|
Loading…
Reference in a new issue