From 637cb82c9cdcb28743e5b99dfa22a12834adca52 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Sun, 16 Mar 2025 21:23:07 +0100 Subject: [PATCH] Alt-drag duplication --- packages/excalidraw/components/App.tsx | 141 +++--------------- .../excalidraw/renderer/interactiveScene.ts | 15 +- 2 files changed, 18 insertions(+), 138 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index e185b91a3..7373991f2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -126,7 +126,6 @@ import { restore, restoreElements } from "../data/restore"; import { dragNewElement, dragSelectedElements, - duplicateElement, getCommonBounds, getCursorForResizingElement, getDragOffsetXY, @@ -152,7 +151,6 @@ import { bindOrUnbindLinearElement, bindOrUnbindLinearElements, fixBindingsAfterDeletion, - fixBindingsAfterDuplication, getHoveredElementForBinding, isBindingEnabled, isLinearElementSimpleAndAlreadyBound, @@ -291,7 +289,6 @@ import { } from "../element/image"; import { fileOpen } from "../data/filesystem"; import { - bindTextToShapeAfterDuplication, getBoundTextElement, getContainerCenter, getContainerElement, @@ -308,7 +305,6 @@ import { Fonts, getLineHeight } from "../fonts"; import { getFrameChildren, isCursorInFrame, - bindElementsToFramesAfterDuplication, addElementsToFrame, replaceAllElementsInFrame, removeElementsFromFrame, @@ -8423,129 +8419,26 @@ class App extends React.Component { pointerDownState.hit.hasBeenDuplicated = true; - const nextElements = []; - const elementsToAppend = []; - const groupIdMap = new Map(); - const oldIdToDuplicatedId = new Map(); const hitElement = pointerDownState.hit.element; - const selectedElementIds = new Set( - this.scene - .getSelectedElements({ - selectedElementIds: this.state.selectedElementIds, - includeBoundTextElement: true, - includeElementsInFrames: true, - }) - .map((element) => element.id), - ); - - const elements = this.scene.getElementsIncludingDeleted(); - - for (const element of elements) { - const isInSelection = - selectedElementIds.has(element.id) || - // case: the state.selectedElementIds might not have been - // updated yet by the time this mousemove event is fired - (element.id === hitElement?.id && - pointerDownState.hit.wasAddedToSelection); - - if (isInSelection) { - const duplicatedElement = duplicateElement( - this.state.editingGroupId, - groupIdMap, - element, - undefined, - true, - ); - - // NOTE (mtolmacs): This is a temporary fix for very large scenes - if ( - Math.abs(duplicatedElement.x) > 1e7 || - Math.abs(duplicatedElement.x) > 1e7 || - Math.abs(duplicatedElement.width) > 1e7 || - Math.abs(duplicatedElement.height) > 1e7 - ) { - console.error( - `Alt+dragging duplicated element with invalid dimensions`, - duplicatedElement.x, - duplicatedElement.y, - duplicatedElement.width, - duplicatedElement.height, - ); - - return; - } - - const origElement = pointerDownState.originalElements.get( - element.id, - )!; - - // NOTE (mtolmacs): This is a temporary fix for very large scenes - if ( - Math.abs(origElement.x) > 1e7 || - Math.abs(origElement.x) > 1e7 || - Math.abs(origElement.width) > 1e7 || - Math.abs(origElement.height) > 1e7 - ) { - console.error( - `Alt+dragging duplicated element with invalid dimensions`, - origElement.x, - origElement.y, - origElement.width, - origElement.height, - ); - - return; - } - - mutateElement(duplicatedElement, { - x: origElement.x, - y: origElement.y, - }); - - // put duplicated element to pointerDownState.originalElements - // so that we can snap to the duplicated element without releasing - pointerDownState.originalElements.set( - duplicatedElement.id, - duplicatedElement, - ); - - nextElements.push(duplicatedElement); - elementsToAppend.push(element); - oldIdToDuplicatedId.set(element.id, duplicatedElement.id); - } else { - nextElements.push(element); - } + const selectedElements = this.scene.getSelectedElements({ + selectedElementIds: this.state.selectedElementIds, + includeBoundTextElement: true, + includeElementsInFrames: true, + }); + if ( + hitElement && + !selectedElements.find((el) => el.id === hitElement.id) + ) { + selectedElements.push(hitElement); } + const clonedElements = duplicateElements(selectedElements, { + appState: this.state, + randomizeSeed: true, + }); - let nextSceneElements: ExcalidrawElement[] = [ - ...nextElements, - ...elementsToAppend, - ]; - - const mappedNewSceneElements = this.props.onDuplicate?.( - nextSceneElements, - elements, - ); - - nextSceneElements = mappedNewSceneElements || nextSceneElements; - - syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend)); - - bindTextToShapeAfterDuplication( - nextElements, - elementsToAppend, - oldIdToDuplicatedId, - ); - fixBindingsAfterDuplication( - nextSceneElements, - elementsToAppend, - oldIdToDuplicatedId, - "duplicatesServeAsOld", - ); - bindElementsToFramesAfterDuplication( - nextSceneElements, - elementsToAppend, - oldIdToDuplicatedId, + const nextSceneElements = syncMovedIndices( + [...clonedElements, ...this.scene.getElementsIncludingDeleted()], + arrayToMap(clonedElements), ); this.scene.replaceAllElements(nextSceneElements); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 887ad424a..b0971f9f2 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -51,13 +51,7 @@ import { } from "../scene/scrollbars"; import { getCornerRadius } from "../shapes"; import { type InteractiveCanvasAppState } from "../types"; -import { - arrayToMap, - invariant, - isDevEnv, - isTestEnv, - throttleRAF, -} from "../utils"; +import { arrayToMap, invariant, throttleRAF } from "../utils"; import { bootstrapCanvas, @@ -895,13 +889,6 @@ const _renderInteractiveScene = ({ // Arrows have a different highlight behavior when // they are the only selected element if (appState.selectedLinearElement) { - if (isTestEnv() || isDevEnv()) { - invariant( - selectedElements.length <= 1, - `There is an active selectedLinearElement on app state but the selectedElements length is ${selectedElements?.length} not 1`, - ); - } - const editor = appState.selectedLinearElement; const firstSelectedLinear = selectedElements.find( (el) => el.id === editor.elementId, // Don't forget bound text elements!