import { Utils } from '@tldraw/core' import { Vec } from '@tldraw/vec' import { TLDR } from '~state/TLDR' import type { PagePartial, TldrawCommand, TDShape } from '~types' import type { TldrawApp } from '../../internal' export function duplicateShapes(app: TldrawApp, ids: string[], point?: number[]): TldrawCommand { const { selectedIds, currentPageId, page, shapes } = app const before: PagePartial = { shapes: {}, bindings: {}, } const after: PagePartial = { shapes: {}, bindings: {}, } const duplicateMap: Record = {} const shapesToDuplicate = ids .map((id) => app.getShape(id)) .filter((shape) => !ids.includes(shape.parentId)) // Create duplicates shapesToDuplicate.forEach((shape) => { const duplicatedId = Utils.uniqueId() before.shapes[duplicatedId] = undefined after.shapes[duplicatedId] = { ...Utils.deepClone(shape), id: duplicatedId, childIndex: TLDR.getChildIndexAbove(app.state, shape.id, currentPageId), } if (shape.children) { after.shapes[duplicatedId]!.children = [] } if (shape.parentId !== currentPageId) { const parent = app.getShape(shape.parentId) before.shapes[parent.id] = { ...before.shapes[parent.id], children: parent.children, } after.shapes[parent.id] = { ...after.shapes[parent.id], children: [...(after.shapes[parent.id] || parent).children!, duplicatedId], } } duplicateMap[shape.id] = duplicatedId }) // If the shapes have children, then duplicate those too shapesToDuplicate.forEach((shape) => { if (shape.children) { shape.children.forEach((childId) => { const child = app.getShape(childId) const duplicatedId = Utils.uniqueId() const duplicatedParentId = duplicateMap[shape.id] before.shapes[duplicatedId] = undefined after.shapes[duplicatedId] = { ...Utils.deepClone(child), id: duplicatedId, parentId: duplicatedParentId, childIndex: TLDR.getChildIndexAbove(app.state, child.id, currentPageId), } duplicateMap[childId] = duplicatedId after.shapes[duplicateMap[shape.id]]?.children?.push(duplicatedId) }) } }) // Which ids did we end up duplicating? const dupedShapeIds = new Set(Object.keys(duplicateMap)) // Handle bindings that effect duplicated shapes Object.values(page.bindings) .filter((binding) => dupedShapeIds.has(binding.fromId) || dupedShapeIds.has(binding.toId)) .forEach((binding) => { if (dupedShapeIds.has(binding.fromId)) { if (dupedShapeIds.has(binding.toId)) { // If the binding is between two duplicating shapes then // duplicate the binding, too const duplicatedBindingId = Utils.uniqueId() const duplicatedBinding = { ...Utils.deepClone(binding), id: duplicatedBindingId, fromId: duplicateMap[binding.fromId], toId: duplicateMap[binding.toId], } before.bindings[duplicatedBindingId] = undefined after.bindings[duplicatedBindingId] = duplicatedBinding // Change the duplicated shape's handle so that it reference // the duplicated binding const boundShape = after.shapes[duplicatedBinding.fromId] Object.values(boundShape!.handles!).forEach((handle) => { if (handle!.bindingId === binding.id) { handle!.bindingId = duplicatedBindingId } }) } else { // If only the fromId is selected, delete the binding on // the duplicated shape's handles const boundShape = after.shapes[duplicateMap[binding.fromId]] Object.values(boundShape!.handles!).forEach((handle) => { if (handle!.bindingId === binding.id) { handle!.bindingId = undefined } }) } } }) // Now move the shapes const shapesToMove = Object.values(after.shapes) as TDShape[] if (point) { const commonBounds = Utils.getCommonBounds(shapesToMove.map((shape) => TLDR.getBounds(shape))) const center = Utils.getBoundsCenter(commonBounds) shapesToMove.forEach((shape) => { // Could be a group if (!shape.point) return shape.point = Vec.sub(point, Vec.sub(center, shape.point)) }) } else { const offset = [16, 16] shapesToMove.forEach((shape) => { // Could be a group if (!shape.point) return shape.point = Vec.add(shape.point, offset) }) } // Unlock any locked shapes shapesToMove.forEach((shape) => { if (shape.isLocked) { shape.isLocked = false } }) return { id: 'duplicate', before: { document: { pages: { [currentPageId]: before, }, pageStates: { [currentPageId]: { selectedIds }, }, }, }, after: { document: { pages: { [currentPageId]: after, }, pageStates: { [currentPageId]: { selectedIds: Array.from(dupedShapeIds.values()).map((id) => duplicateMap[id]), }, }, }, }, } }