Alt-drag duplication

This commit is contained in:
Mark Tolmacs 2025-03-16 21:23:07 +01:00
parent 49dcf23101
commit 637cb82c9c
2 changed files with 18 additions and 138 deletions

View file

@ -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<AppProps, AppState> {
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);

View file

@ -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!