diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index 784d6c4b0..101e36f7b 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -9,8 +9,6 @@ import { } from "../element/textElement"; import { hasBoundTextElement, - isArrowElement, - isBindableElement, isBoundToContainer, isFrameLikeElement, } from "../element/typeChecks"; @@ -41,18 +39,13 @@ import { invariant, } from "../utils"; -import { type ElementUpdate, mutateElement } from "../element/mutateElement"; +import { duplicateElements } from "../element/duplicate"; import { register } from "./register"; import type { ActionResult } from "./types"; -import type { - BoundElement, - ExcalidrawArrowElement, - ExcalidrawElement, -} from "../element/types"; +import type { ExcalidrawElement } from "../element/types"; import type { AppState } from "../types"; -import type { Mutable } from "../utility-types"; export const actionDuplicateSelection = register({ name: "duplicateSelection", @@ -83,20 +76,51 @@ export const actionDuplicateSelection = register({ } } - const nextState = duplicateElements(elements, appState); + const origElements: ExcalidrawElement[] = elements.slice(); + const clonedElements = duplicateElements(elements, { + randomizeSeed: true, + overrides: (element) => ({ + x: element.x + DEFAULT_GRID_SIZE / 2, + y: element.y + DEFAULT_GRID_SIZE / 2, + }), + }); - if (app.props.onDuplicate && nextState.elements) { - const mappedElements = app.props.onDuplicate( - nextState.elements, - elements, - ); + let nextElements = origElements.concat(clonedElements); + + if (app.props.onDuplicate && nextElements) { + const mappedElements = app.props.onDuplicate(nextElements, elements); if (mappedElements) { - nextState.elements = mappedElements; + nextElements = mappedElements; } } + //nextElements = syncMovedIndices(nextElements, arrayToMap(clonedElements)); + + const nextElementsToSelect = + excludeElementsInFramesFromSelection(clonedElements); + return { - ...nextState, + elements: nextElements, + appState: { + ...appState, + ...selectGroupsForSelectedElements( + { + editingGroupId: appState.editingGroupId, + selectedElementIds: nextElementsToSelect.reduce( + (acc: Record, element) => { + if (!isBoundToContainer(element)) { + acc[element.id] = true; + } + return acc; + }, + {}, + ), + }, + getNonDeletedElements(nextElements), + appState, + null, + ), + }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, }; }, @@ -115,7 +139,7 @@ export const actionDuplicateSelection = register({ ), }); -const duplicateElements = ( +const _duplicateElements = ( elements: readonly ExcalidrawElement[], appState: AppState, ): Partial> => { @@ -339,96 +363,96 @@ const duplicateElements = ( // oldIdToDuplicatedId, // ); - newElements - .map((element) => { - oldElements.includes(element) && console.error("oldElements", element); + // newElements + // .map((element) => { + // oldElements.includes(element) && console.error("oldElements", element); - if (isArrowElement(element)) { - const updates: Mutable> = {}; + // if (isArrowElement(element)) { + // const updates: Mutable> = {}; - if (element.startBinding) { - const startCloneElementId = oldIdToDuplicatedId.get( - element.startBinding.elementId, - ); + // if (element.startBinding) { + // const startCloneElementId = oldIdToDuplicatedId.get( + // element.startBinding.elementId, + // ); - if (startCloneElementId) { - // The connected element was duplicated, so we need to update the binding - updates.startBinding = { - ...element.startBinding, - elementId: startCloneElementId, - }; - } else { - // The connected element was not duplicated, so we need to remove the binding - updates.startBinding = null; - } - } + // if (startCloneElementId) { + // // The connected element was duplicated, so we need to update the binding + // updates.startBinding = { + // ...element.startBinding, + // elementId: startCloneElementId, + // }; + // } else { + // // The connected element was not duplicated, so we need to remove the binding + // updates.startBinding = null; + // } + // } - if (element.endBinding) { - const endCloneElementId = oldIdToDuplicatedId.get( - element.endBinding.elementId, - ); + // if (element.endBinding) { + // const endCloneElementId = oldIdToDuplicatedId.get( + // element.endBinding.elementId, + // ); - if (endCloneElementId) { - // The connected element was duplicated, so we need to update the binding - updates.endBinding = { - ...element.endBinding, - elementId: endCloneElementId, - }; - } else { - // The connected element was not duplicated, so we need to remove the binding - updates.endBinding = null; - } - } + // if (endCloneElementId) { + // // The connected element was duplicated, so we need to update the binding + // updates.endBinding = { + // ...element.endBinding, + // elementId: endCloneElementId, + // }; + // } else { + // // The connected element was not duplicated, so we need to remove the binding + // updates.endBinding = null; + // } + // } - if (Object.keys(updates).length > 0) { - // Only update the element if there are updates to apply - return { - element, - updates, - }; - } - } else if (isBindableElement(element)) { - if (element.boundElements?.length) { - const clonedBoundElements = element.boundElements - ?.map((definition) => { - const clonedBoundElementId = oldIdToDuplicatedId.get( - definition.id, - ); - if (clonedBoundElementId) { - // The connected element was duplicated, so we need to update the binding - return { - ...definition, - id: clonedBoundElementId, - }; - } + // if (Object.keys(updates).length > 0) { + // // Only update the element if there are updates to apply + // return { + // element, + // updates, + // }; + // } + // } else if (isBindableElement(element)) { + // if (element.boundElements?.length) { + // const clonedBoundElements = element.boundElements + // ?.map((definition) => { + // const clonedBoundElementId = oldIdToDuplicatedId.get( + // definition.id, + // ); + // if (clonedBoundElementId) { + // // The connected element was duplicated, so we need to update the binding + // return { + // ...definition, + // id: clonedBoundElementId, + // }; + // } - // The connected element was not duplicated, so we need to remove the binding - return null; - }) - .filter( - (definition): definition is BoundElement => definition !== null, - ); + // // The connected element was not duplicated, so we need to remove the binding + // return null; + // }) + // .filter( + // (definition): definition is BoundElement => definition !== null, + // ); - if (clonedBoundElements?.length) { - return { - element, - updates: { - boundElements: clonedBoundElements, - }, - }; - } - } - } + // if (clonedBoundElements?.length) { + // return { + // element, + // updates: { + // boundElements: clonedBoundElements, + // }, + // }; + // } + // } + // } - return null; - }) - .forEach((change) => { - if (!change) { - return; - } + // return null; + // }) + // .forEach((change) => { + // if (!change) { + // return; + // } - mutateElement(change.element, change.updates); - }); + // mutateElement(change.element, change.updates); + // }); bindElementsToFramesAfterDuplication( elementsWithClones, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 77a823d9a..b874e0135 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -8445,24 +8445,6 @@ class App extends React.Component { // updated yet by the time this mousemove event is fired (element.id === hitElement?.id && pointerDownState.hit.wasAddedToSelection); - // NOTE (mtolmacs): This is a temporary fix for very large scenes - if ( - Math.abs(element.x) > 1e7 || - Math.abs(element.x) > 1e7 || - Math.abs(element.width) > 1e7 || - Math.abs(element.height) > 1e7 - ) { - console.error( - `Alt+dragging element in scene with invalid dimensions`, - element.x, - element.y, - element.width, - element.height, - isInSelection, - ); - - return; - } if (isInSelection) { const duplicatedElement = duplicateElement( diff --git a/packages/excalidraw/element/duplicate.ts b/packages/excalidraw/element/duplicate.ts index a0243953f..fc4875dde 100644 --- a/packages/excalidraw/element/duplicate.ts +++ b/packages/excalidraw/element/duplicate.ts @@ -5,6 +5,7 @@ import type { AppState } from "../types"; import type { Mutable } from "../utility-types"; import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils"; import { bumpVersion } from "./mutateElement"; +import { isArrowElement } from "./typeChecks"; import type { ExcalidrawElement, GroupId } from "./types"; /** @@ -66,7 +67,8 @@ export const duplicateElements = ( elements: readonly ExcalidrawElement[], opts?: { /** NOTE also updates version flags and `updated` */ - randomizeSeed: boolean; + randomizeSeed?: boolean; + overrides?: (element: ExcalidrawElement) => Partial; }, ) => { const clonedElements: ExcalidrawElement[] = []; @@ -79,7 +81,7 @@ export const duplicateElements = ( /* new */ ExcalidrawElement["id"] >(); - const maybeGetNewId = (id: ExcalidrawElement["id"]) => { + const maybeGetNewIdFor = (id: ExcalidrawElement["id"]) => { // if we've already migrated the element id, return the new one directly if (elementNewIdsMap.has(id)) { return elementNewIdsMap.get(id)!; @@ -98,9 +100,16 @@ export const duplicateElements = ( const groupNewIdsMap = new Map(); for (const element of elements) { - const clonedElement: Mutable = _deepCopyElement(element); + let clonedElement: Mutable = _deepCopyElement(element); - clonedElement.id = maybeGetNewId(element.id)!; + if (opts?.overrides) { + clonedElement = Object.assign( + clonedElement, + opts.overrides(clonedElement), + ); + } + + clonedElement.id = maybeGetNewIdFor(element.id)!; if (isTestEnv()) { __test__defineOrigId(clonedElement, element.id); } @@ -120,7 +129,7 @@ export const duplicateElements = ( } if ("containerId" in clonedElement && clonedElement.containerId) { - const newContainerId = maybeGetNewId(clonedElement.containerId); + const newContainerId = maybeGetNewIdFor(clonedElement.containerId); clonedElement.containerId = newContainerId; } @@ -130,7 +139,7 @@ export const duplicateElements = ( acc: Mutable>, binding, ) => { - const newBindingId = maybeGetNewId(binding.id); + const newBindingId = maybeGetNewIdFor(binding.id); if (newBindingId) { acc.push({ ...binding, id: newBindingId }); } @@ -141,7 +150,9 @@ export const duplicateElements = ( } if ("endBinding" in clonedElement && clonedElement.endBinding) { - const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId); + const newEndBindingId = maybeGetNewIdFor( + clonedElement.endBinding.elementId, + ); clonedElement.endBinding = newEndBindingId ? { ...clonedElement.endBinding, @@ -150,7 +161,7 @@ export const duplicateElements = ( : null; } if ("startBinding" in clonedElement && clonedElement.startBinding) { - const newEndBindingId = maybeGetNewId( + const newEndBindingId = maybeGetNewIdFor( clonedElement.startBinding.elementId, ); clonedElement.startBinding = newEndBindingId @@ -162,7 +173,7 @@ export const duplicateElements = ( } if (clonedElement.frameId) { - clonedElement.frameId = maybeGetNewId(clonedElement.frameId); + clonedElement.frameId = maybeGetNewIdFor(clonedElement.frameId); } clonedElements.push(clonedElement);