You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

duplicate.command.ts 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { Utils } from '@tldraw/core'
  3. import { Vec } from '@tldraw/vec'
  4. import { TLDR } from '~state/tldr'
  5. import type { Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
  6. export function duplicate(data: Data, ids: string[], point?: number[]): TLDrawCommand {
  7. const { currentPageId } = data.appState
  8. const page = TLDR.getPage(data, currentPageId)
  9. const before: PagePartial = {
  10. shapes: {},
  11. bindings: {},
  12. }
  13. const after: PagePartial = {
  14. shapes: {},
  15. bindings: {},
  16. }
  17. const shapes = TLDR.getSelectedIds(data, currentPageId).map((id) =>
  18. TLDR.getShape(data, id, currentPageId)
  19. )
  20. const duplicateMap: Record<string, string> = {}
  21. // Create duplicates
  22. shapes
  23. .filter((shape) => !ids.includes(shape.parentId))
  24. .forEach((shape) => {
  25. const duplicatedId = Utils.uniqueId()
  26. before.shapes[duplicatedId] = undefined
  27. after.shapes[duplicatedId] = {
  28. ...Utils.deepClone(shape),
  29. id: duplicatedId,
  30. childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId),
  31. }
  32. if (shape.children) {
  33. after.shapes[duplicatedId]!.children = []
  34. }
  35. if (shape.parentId !== currentPageId) {
  36. const parent = TLDR.getShape(data, shape.parentId, currentPageId)
  37. before.shapes[parent.id] = {
  38. ...before.shapes[parent.id],
  39. children: parent.children,
  40. }
  41. after.shapes[parent.id] = {
  42. ...after.shapes[parent.id],
  43. children: [...(after.shapes[parent.id] || parent).children!, duplicatedId],
  44. }
  45. }
  46. duplicateMap[shape.id] = duplicatedId
  47. })
  48. // If the shapes have children, then duplicate those too
  49. shapes.forEach((shape) => {
  50. if (shape.children) {
  51. shape.children.forEach((childId) => {
  52. const child = TLDR.getShape(data, childId, currentPageId)
  53. const duplicatedId = Utils.uniqueId()
  54. const duplicatedParentId = duplicateMap[shape.id]
  55. before.shapes[duplicatedId] = undefined
  56. after.shapes[duplicatedId] = {
  57. ...Utils.deepClone(child),
  58. id: duplicatedId,
  59. parentId: duplicatedParentId,
  60. childIndex: TLDR.getChildIndexAbove(data, child.id, currentPageId),
  61. }
  62. duplicateMap[childId] = duplicatedId
  63. after.shapes[duplicateMap[shape.id]]?.children?.push(duplicatedId)
  64. })
  65. }
  66. })
  67. // Which ids did we end up duplicating?
  68. const dupedShapeIds = new Set(Object.keys(duplicateMap))
  69. // Handle bindings that effect duplicated shapes
  70. Object.values(page.bindings)
  71. .filter((binding) => dupedShapeIds.has(binding.fromId) || dupedShapeIds.has(binding.toId))
  72. .forEach((binding) => {
  73. if (dupedShapeIds.has(binding.fromId)) {
  74. if (dupedShapeIds.has(binding.toId)) {
  75. // If the binding is between two duplicating shapes then
  76. // duplicate the binding, too
  77. const duplicatedBindingId = Utils.uniqueId()
  78. const duplicatedBinding = {
  79. ...Utils.deepClone(binding),
  80. id: duplicatedBindingId,
  81. fromId: duplicateMap[binding.fromId],
  82. toId: duplicateMap[binding.toId],
  83. }
  84. before.bindings[duplicatedBindingId] = undefined
  85. after.bindings[duplicatedBindingId] = duplicatedBinding
  86. // Change the duplicated shape's handle so that it reference
  87. // the duplicated binding
  88. const boundShape = after.shapes[duplicatedBinding.fromId]
  89. Object.values(boundShape!.handles!).forEach((handle) => {
  90. if (handle!.bindingId === binding.id) {
  91. handle!.bindingId = duplicatedBindingId
  92. }
  93. })
  94. } else {
  95. // If only the fromId is selected, delete the binding on
  96. // the duplicated shape's handles
  97. const boundShape = after.shapes[duplicateMap[binding.fromId]]
  98. Object.values(boundShape!.handles!).forEach((handle) => {
  99. if (handle!.bindingId === binding.id) {
  100. handle!.bindingId = undefined
  101. }
  102. })
  103. }
  104. }
  105. })
  106. // Now move the shapes
  107. const shapesToMove = Object.values(after.shapes) as TLDrawShape[]
  108. if (point) {
  109. const commonBounds = Utils.getCommonBounds(shapesToMove.map((shape) => TLDR.getBounds(shape)))
  110. const center = Utils.getBoundsCenter(commonBounds)
  111. shapesToMove.forEach((shape) => {
  112. // Could be a group
  113. if (!shape.point) return
  114. shape.point = Vec.sub(point, Vec.sub(center, shape.point))
  115. })
  116. } else {
  117. const offset = [16, 16] // Vec.div([16, 16], data.document.pageStates[page.id].camera.zoom)
  118. shapesToMove.forEach((shape) => {
  119. // Could be a group
  120. if (!shape.point) return
  121. shape.point = Vec.add(shape.point, offset)
  122. })
  123. }
  124. return {
  125. id: 'duplicate',
  126. before: {
  127. document: {
  128. pages: {
  129. [currentPageId]: before,
  130. },
  131. pageStates: {
  132. [currentPageId]: { selectedIds: ids },
  133. },
  134. },
  135. },
  136. after: {
  137. document: {
  138. pages: {
  139. [currentPageId]: after,
  140. },
  141. pageStates: {
  142. [currentPageId]: {
  143. selectedIds: Array.from(dupedShapeIds.values()).map((id) => duplicateMap[id]),
  144. },
  145. },
  146. },
  147. },
  148. }
  149. }