kopia lustrzana https://github.com/Tldraw/Tldraw
176 wiersze
5.1 KiB
TypeScript
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]),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|