Tldraw/packages/tldraw/src/state/commands/duplicateShapes/duplicateShapes.ts

176 wiersze
5.1 KiB
TypeScript

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<string, string> = {}
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]),
},
},
},
},
}
}