Duplication action refactor

This commit is contained in:
Mark Tolmacs 2025-03-11 12:35:35 +01:00
parent 4cfcd4b353
commit c4767a3144
3 changed files with 142 additions and 125 deletions

View file

@ -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<ExcalidrawElement["id"], true>, 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<Exclude<ActionResult, false>> => {
@ -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<ElementUpdate<ExcalidrawArrowElement>> = {};
// if (isArrowElement(element)) {
// const updates: Mutable<ElementUpdate<ExcalidrawArrowElement>> = {};
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,

View file

@ -8445,24 +8445,6 @@ class App extends React.Component<AppProps, AppState> {
// 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(

View file

@ -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<ExcalidrawElement>;
},
) => {
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</* orig */ GroupId, /* new */ GroupId>();
for (const element of elements) {
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
let clonedElement: Mutable<ExcalidrawElement> = _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<NonNullable<ExcalidrawElement["boundElements"]>>,
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);