mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Duplication action refactor
This commit is contained in:
parent
4cfcd4b353
commit
c4767a3144
3 changed files with 142 additions and 125 deletions
|
@ -9,8 +9,6 @@ import {
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isArrowElement,
|
|
||||||
isBindableElement,
|
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
|
@ -41,18 +39,13 @@ import {
|
||||||
invariant,
|
invariant,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { type ElementUpdate, mutateElement } from "../element/mutateElement";
|
import { duplicateElements } from "../element/duplicate";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { ActionResult } from "./types";
|
import type { ActionResult } from "./types";
|
||||||
import type {
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
BoundElement,
|
|
||||||
ExcalidrawArrowElement,
|
|
||||||
ExcalidrawElement,
|
|
||||||
} from "../element/types";
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
name: "duplicateSelection",
|
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) {
|
let nextElements = origElements.concat(clonedElements);
|
||||||
const mappedElements = app.props.onDuplicate(
|
|
||||||
nextState.elements,
|
if (app.props.onDuplicate && nextElements) {
|
||||||
elements,
|
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
||||||
);
|
|
||||||
if (mappedElements) {
|
if (mappedElements) {
|
||||||
nextState.elements = mappedElements;
|
nextElements = mappedElements;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nextElements = syncMovedIndices(nextElements, arrayToMap(clonedElements));
|
||||||
|
|
||||||
|
const nextElementsToSelect =
|
||||||
|
excludeElementsInFramesFromSelection(clonedElements);
|
||||||
|
|
||||||
return {
|
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,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -115,7 +139,7 @@ export const actionDuplicateSelection = register({
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const duplicateElements = (
|
const _duplicateElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): Partial<Exclude<ActionResult, false>> => {
|
): Partial<Exclude<ActionResult, false>> => {
|
||||||
|
@ -339,96 +363,96 @@ const duplicateElements = (
|
||||||
// oldIdToDuplicatedId,
|
// oldIdToDuplicatedId,
|
||||||
// );
|
// );
|
||||||
|
|
||||||
newElements
|
// newElements
|
||||||
.map((element) => {
|
// .map((element) => {
|
||||||
oldElements.includes(element) && console.error("oldElements", element);
|
// oldElements.includes(element) && console.error("oldElements", element);
|
||||||
|
|
||||||
if (isArrowElement(element)) {
|
// if (isArrowElement(element)) {
|
||||||
const updates: Mutable<ElementUpdate<ExcalidrawArrowElement>> = {};
|
// const updates: Mutable<ElementUpdate<ExcalidrawArrowElement>> = {};
|
||||||
|
|
||||||
if (element.startBinding) {
|
// if (element.startBinding) {
|
||||||
const startCloneElementId = oldIdToDuplicatedId.get(
|
// const startCloneElementId = oldIdToDuplicatedId.get(
|
||||||
element.startBinding.elementId,
|
// element.startBinding.elementId,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (startCloneElementId) {
|
// if (startCloneElementId) {
|
||||||
// The connected element was duplicated, so we need to update the binding
|
// // The connected element was duplicated, so we need to update the binding
|
||||||
updates.startBinding = {
|
// updates.startBinding = {
|
||||||
...element.startBinding,
|
// ...element.startBinding,
|
||||||
elementId: startCloneElementId,
|
// elementId: startCloneElementId,
|
||||||
};
|
// };
|
||||||
} else {
|
// } else {
|
||||||
// The connected element was not duplicated, so we need to remove the binding
|
// // The connected element was not duplicated, so we need to remove the binding
|
||||||
updates.startBinding = null;
|
// updates.startBinding = null;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (element.endBinding) {
|
// if (element.endBinding) {
|
||||||
const endCloneElementId = oldIdToDuplicatedId.get(
|
// const endCloneElementId = oldIdToDuplicatedId.get(
|
||||||
element.endBinding.elementId,
|
// element.endBinding.elementId,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (endCloneElementId) {
|
// if (endCloneElementId) {
|
||||||
// The connected element was duplicated, so we need to update the binding
|
// // The connected element was duplicated, so we need to update the binding
|
||||||
updates.endBinding = {
|
// updates.endBinding = {
|
||||||
...element.endBinding,
|
// ...element.endBinding,
|
||||||
elementId: endCloneElementId,
|
// elementId: endCloneElementId,
|
||||||
};
|
// };
|
||||||
} else {
|
// } else {
|
||||||
// The connected element was not duplicated, so we need to remove the binding
|
// // The connected element was not duplicated, so we need to remove the binding
|
||||||
updates.endBinding = null;
|
// updates.endBinding = null;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) {
|
// if (Object.keys(updates).length > 0) {
|
||||||
// Only update the element if there are updates to apply
|
// // Only update the element if there are updates to apply
|
||||||
return {
|
// return {
|
||||||
element,
|
// element,
|
||||||
updates,
|
// updates,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
} else if (isBindableElement(element)) {
|
// } else if (isBindableElement(element)) {
|
||||||
if (element.boundElements?.length) {
|
// if (element.boundElements?.length) {
|
||||||
const clonedBoundElements = element.boundElements
|
// const clonedBoundElements = element.boundElements
|
||||||
?.map((definition) => {
|
// ?.map((definition) => {
|
||||||
const clonedBoundElementId = oldIdToDuplicatedId.get(
|
// const clonedBoundElementId = oldIdToDuplicatedId.get(
|
||||||
definition.id,
|
// definition.id,
|
||||||
);
|
// );
|
||||||
if (clonedBoundElementId) {
|
// if (clonedBoundElementId) {
|
||||||
// The connected element was duplicated, so we need to update the binding
|
// // The connected element was duplicated, so we need to update the binding
|
||||||
return {
|
// return {
|
||||||
...definition,
|
// ...definition,
|
||||||
id: clonedBoundElementId,
|
// id: clonedBoundElementId,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
// The connected element was not duplicated, so we need to remove the binding
|
// // The connected element was not duplicated, so we need to remove the binding
|
||||||
return null;
|
// return null;
|
||||||
})
|
// })
|
||||||
.filter(
|
// .filter(
|
||||||
(definition): definition is BoundElement => definition !== null,
|
// (definition): definition is BoundElement => definition !== null,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (clonedBoundElements?.length) {
|
// if (clonedBoundElements?.length) {
|
||||||
return {
|
// return {
|
||||||
element,
|
// element,
|
||||||
updates: {
|
// updates: {
|
||||||
boundElements: clonedBoundElements,
|
// boundElements: clonedBoundElements,
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
})
|
// })
|
||||||
.forEach((change) => {
|
// .forEach((change) => {
|
||||||
if (!change) {
|
// if (!change) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
mutateElement(change.element, change.updates);
|
// mutateElement(change.element, change.updates);
|
||||||
});
|
// });
|
||||||
|
|
||||||
bindElementsToFramesAfterDuplication(
|
bindElementsToFramesAfterDuplication(
|
||||||
elementsWithClones,
|
elementsWithClones,
|
||||||
|
|
|
@ -8445,24 +8445,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// updated yet by the time this mousemove event is fired
|
// updated yet by the time this mousemove event is fired
|
||||||
(element.id === hitElement?.id &&
|
(element.id === hitElement?.id &&
|
||||||
pointerDownState.hit.wasAddedToSelection);
|
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) {
|
if (isInSelection) {
|
||||||
const duplicatedElement = duplicateElement(
|
const duplicatedElement = duplicateElement(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { AppState } from "../types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||||
import { bumpVersion } from "./mutateElement";
|
import { bumpVersion } from "./mutateElement";
|
||||||
|
import { isArrowElement } from "./typeChecks";
|
||||||
import type { ExcalidrawElement, GroupId } from "./types";
|
import type { ExcalidrawElement, GroupId } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,7 +67,8 @@ export const duplicateElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
opts?: {
|
opts?: {
|
||||||
/** NOTE also updates version flags and `updated` */
|
/** NOTE also updates version flags and `updated` */
|
||||||
randomizeSeed: boolean;
|
randomizeSeed?: boolean;
|
||||||
|
overrides?: (element: ExcalidrawElement) => Partial<ExcalidrawElement>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const clonedElements: ExcalidrawElement[] = [];
|
const clonedElements: ExcalidrawElement[] = [];
|
||||||
|
@ -79,7 +81,7 @@ export const duplicateElements = (
|
||||||
/* new */ ExcalidrawElement["id"]
|
/* 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 we've already migrated the element id, return the new one directly
|
||||||
if (elementNewIdsMap.has(id)) {
|
if (elementNewIdsMap.has(id)) {
|
||||||
return elementNewIdsMap.get(id)!;
|
return elementNewIdsMap.get(id)!;
|
||||||
|
@ -98,9 +100,16 @@ export const duplicateElements = (
|
||||||
const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
|
const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
|
||||||
|
|
||||||
for (const element of elements) {
|
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()) {
|
if (isTestEnv()) {
|
||||||
__test__defineOrigId(clonedElement, element.id);
|
__test__defineOrigId(clonedElement, element.id);
|
||||||
}
|
}
|
||||||
|
@ -120,7 +129,7 @@ export const duplicateElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("containerId" in clonedElement && clonedElement.containerId) {
|
if ("containerId" in clonedElement && clonedElement.containerId) {
|
||||||
const newContainerId = maybeGetNewId(clonedElement.containerId);
|
const newContainerId = maybeGetNewIdFor(clonedElement.containerId);
|
||||||
clonedElement.containerId = newContainerId;
|
clonedElement.containerId = newContainerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +139,7 @@ export const duplicateElements = (
|
||||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||||
binding,
|
binding,
|
||||||
) => {
|
) => {
|
||||||
const newBindingId = maybeGetNewId(binding.id);
|
const newBindingId = maybeGetNewIdFor(binding.id);
|
||||||
if (newBindingId) {
|
if (newBindingId) {
|
||||||
acc.push({ ...binding, id: newBindingId });
|
acc.push({ ...binding, id: newBindingId });
|
||||||
}
|
}
|
||||||
|
@ -141,7 +150,9 @@ export const duplicateElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("endBinding" in clonedElement && clonedElement.endBinding) {
|
if ("endBinding" in clonedElement && clonedElement.endBinding) {
|
||||||
const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
|
const newEndBindingId = maybeGetNewIdFor(
|
||||||
|
clonedElement.endBinding.elementId,
|
||||||
|
);
|
||||||
clonedElement.endBinding = newEndBindingId
|
clonedElement.endBinding = newEndBindingId
|
||||||
? {
|
? {
|
||||||
...clonedElement.endBinding,
|
...clonedElement.endBinding,
|
||||||
|
@ -150,7 +161,7 @@ export const duplicateElements = (
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
if ("startBinding" in clonedElement && clonedElement.startBinding) {
|
if ("startBinding" in clonedElement && clonedElement.startBinding) {
|
||||||
const newEndBindingId = maybeGetNewId(
|
const newEndBindingId = maybeGetNewIdFor(
|
||||||
clonedElement.startBinding.elementId,
|
clonedElement.startBinding.elementId,
|
||||||
);
|
);
|
||||||
clonedElement.startBinding = newEndBindingId
|
clonedElement.startBinding = newEndBindingId
|
||||||
|
@ -162,7 +173,7 @@ export const duplicateElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clonedElement.frameId) {
|
if (clonedElement.frameId) {
|
||||||
clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
|
clonedElement.frameId = maybeGetNewIdFor(clonedElement.frameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
clonedElements.push(clonedElement);
|
clonedElements.push(clonedElement);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue