mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: Refactor and merge duplication and binding (#9246)
All checks were successful
Tests / test (push) Successful in 4m52s
All checks were successful
Tests / test (push) Successful in 4m52s
This commit is contained in:
parent
58990b41ae
commit
77aca48c84
29 changed files with 1293 additions and 1085 deletions
|
@ -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,
|
||||
|
@ -163,9 +161,8 @@ import {
|
|||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { deepCopyElement, duplicateElements } from "../element/duplicate";
|
||||
import {
|
||||
deepCopyElement,
|
||||
duplicateElements,
|
||||
newFrameElement,
|
||||
newFreeDrawElement,
|
||||
newEmbeddableElement,
|
||||
|
@ -292,7 +289,6 @@ import {
|
|||
} from "../element/image";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import {
|
||||
bindTextToShapeAfterDuplication,
|
||||
getBoundTextElement,
|
||||
getContainerCenter,
|
||||
getContainerElement,
|
||||
|
@ -309,7 +305,6 @@ import { Fonts, getLineHeight } from "../fonts";
|
|||
import {
|
||||
getFrameChildren,
|
||||
isCursorInFrame,
|
||||
bindElementsToFramesAfterDuplication,
|
||||
addElementsToFrame,
|
||||
replaceAllElementsInFrame,
|
||||
removeElementsFromFrame,
|
||||
|
@ -3224,17 +3219,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||
|
||||
const newElements = duplicateElements(
|
||||
elements.map((element) => {
|
||||
const { newElements } = duplicateElements({
|
||||
type: "everything",
|
||||
elements: elements.map((element) => {
|
||||
return newElementWith(element, {
|
||||
x: element.x + gridX - minX,
|
||||
y: element.y + gridY - minY,
|
||||
});
|
||||
}),
|
||||
{
|
||||
randomizeSeed: !opts.retainSeed,
|
||||
},
|
||||
);
|
||||
randomizeSeed: !opts.retainSeed,
|
||||
});
|
||||
|
||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = [...prevElements, ...newElements];
|
||||
|
@ -6095,7 +6089,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setState({
|
||||
activeEmbeddable: { element: hitElement, state: "hover" },
|
||||
});
|
||||
} else if (!hitElement || !isElbowArrow(hitElement)) {
|
||||
} else if (
|
||||
!hitElement ||
|
||||
// Ebow arrows can only be moved when unconnected
|
||||
!isElbowArrow(hitElement) ||
|
||||
!(hitElement.startBinding || hitElement.endBinding)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
if (this.state.activeEmbeddable?.state === "hover") {
|
||||
this.setState({ activeEmbeddable: null });
|
||||
|
@ -6288,7 +6287,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
if (
|
||||
// Ebow arrows can only be moved when unconnected
|
||||
!isElbowArrow(element) ||
|
||||
!(element.startBinding || element.endBinding)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
}
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
if (
|
||||
|
@ -8138,6 +8143,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
...this.state.selectedLinearElement,
|
||||
pointerDownState: ret.pointerDownState,
|
||||
selectedPointsIndices: ret.selectedPointsIndices,
|
||||
segmentMidPointHoveredCoords: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -8147,6 +8153,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
...this.state.editingLinearElement,
|
||||
pointerDownState: ret.pointerDownState,
|
||||
selectedPointsIndices: ret.selectedPointsIndices,
|
||||
segmentMidPointHoveredCoords: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -8160,7 +8167,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const didDrag = LinearElementEditor.handlePointDragging(
|
||||
const newLinearElementEditor = LinearElementEditor.handlePointDragging(
|
||||
event,
|
||||
this,
|
||||
pointerCoords.x,
|
||||
|
@ -8174,29 +8181,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
linearElementEditor,
|
||||
this.scene,
|
||||
);
|
||||
if (didDrag) {
|
||||
if (newLinearElementEditor) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
if (
|
||||
this.state.editingLinearElement &&
|
||||
!this.state.editingLinearElement.isDragging
|
||||
) {
|
||||
this.setState({
|
||||
editingLinearElement: {
|
||||
...this.state.editingLinearElement,
|
||||
isDragging: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!this.state.selectedLinearElement.isDragging) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
isDragging: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
editingLinearElement: this.state.editingLinearElement
|
||||
? newLinearElementEditor
|
||||
: null,
|
||||
selectedLinearElement: newLinearElementEditor,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -8422,145 +8418,55 @@ 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);
|
||||
// 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(
|
||||
this.state.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
);
|
||||
|
||||
// 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 hitElement = pointerDownState.hit.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);
|
||||
}
|
||||
|
||||
let nextSceneElements: ExcalidrawElement[] = [
|
||||
...nextElements,
|
||||
...elementsToAppend,
|
||||
];
|
||||
const { newElements: clonedElements, elementsWithClones } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate: new Map(
|
||||
selectedElements.map((el) => [el.id, el]),
|
||||
),
|
||||
overrides: (el) => {
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
|
||||
if (origEl) {
|
||||
return {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
reverseOrder: true,
|
||||
});
|
||||
clonedElements.forEach((element) => {
|
||||
pointerDownState.originalElements.set(element.id, element);
|
||||
});
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
nextSceneElements,
|
||||
elementsWithClones,
|
||||
elements,
|
||||
);
|
||||
|
||||
nextSceneElements = mappedNewSceneElements || nextSceneElements;
|
||||
|
||||
syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
|
||||
|
||||
bindTextToShapeAfterDuplication(
|
||||
nextElements,
|
||||
elementsToAppend,
|
||||
oldIdToDuplicatedId,
|
||||
);
|
||||
fixBindingsAfterDuplication(
|
||||
nextSceneElements,
|
||||
elementsToAppend,
|
||||
oldIdToDuplicatedId,
|
||||
"duplicatesServeAsOld",
|
||||
);
|
||||
bindElementsToFramesAfterDuplication(
|
||||
nextSceneElements,
|
||||
elementsToAppend,
|
||||
oldIdToDuplicatedId,
|
||||
const nextSceneElements = syncMovedIndices(
|
||||
mappedNewSceneElements || elementsWithClones,
|
||||
arrayToMap(clonedElements),
|
||||
);
|
||||
|
||||
this.scene.replaceAllElements(nextSceneElements);
|
||||
|
|
|
@ -8,18 +8,20 @@ import React, {
|
|||
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { serializeLibraryAsJSON } from "../data/json";
|
||||
import { duplicateElements } from "../element/newElement";
|
||||
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
|
||||
import { useScrollPosition } from "../hooks/useScrollPosition";
|
||||
import { t } from "../i18n";
|
||||
import { arrayToMap } from "../utils";
|
||||
|
||||
import { duplicateElements } from "../element/duplicate";
|
||||
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
|
||||
import {
|
||||
LibraryMenuSection,
|
||||
LibraryMenuSectionGrid,
|
||||
} from "./LibraryMenuSection";
|
||||
|
||||
import Spinner from "./Spinner";
|
||||
import Stack from "./Stack";
|
||||
|
||||
|
@ -160,7 +162,11 @@ export default function LibraryMenuItems({
|
|||
...item,
|
||||
// duplicate each library item before inserting on canvas to confine
|
||||
// ids and bindings to each library item. See #6465
|
||||
elements: duplicateElements(item.elements, { randomizeSeed: true }),
|
||||
elements: duplicateElements({
|
||||
type: "everything",
|
||||
elements: item.elements,
|
||||
randomizeSeed: true,
|
||||
}).newElements,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { deepCopyElement } from "@excalidraw/excalidraw/element/duplicate";
|
||||
|
||||
import { EVENT } from "../../constants";
|
||||
import { deepCopyElement } from "../../element/newElement";
|
||||
import { KEYS } from "../../keys";
|
||||
import { CaptureUpdateAction } from "../../store";
|
||||
import { cloneJSON } from "../../utils";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue