Deprecate mutateElement in the codebase

This commit is contained in:
Marcel Mraz 2025-04-15 23:53:43 +02:00
parent acfa33650e
commit 11600ee6a6
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
27 changed files with 100 additions and 140 deletions

View file

@ -48,7 +48,7 @@ import {
type Heading, type Heading,
} from "./heading"; } from "./heading";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElementWith, mutateElement } from "./mutateElement"; import { mutateElementWith } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isArrowElement, isArrowElement,
@ -848,7 +848,7 @@ export const updateBoundElements = (
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !boundText.isDeleted) { if (boundText && !boundText.isDeleted) {
handleBindTextResize(element, elementsMap, false); handleBindTextResize(element, scene, false);
} }
}); });
}; };
@ -1499,7 +1499,7 @@ const fixReversedBindingsForBindables = (
(el) => el.id === newArrowId, (el) => el.id === newArrowId,
)! as ExcalidrawArrowElement; )! as ExcalidrawArrowElement;
mutateElement(newArrow, { mutateElementWith(newArrow, originalElements, {
startBinding: startBinding:
oldArrow.startBinding?.elementId === binding.id oldArrow.startBinding?.elementId === binding.id
? { ? {
@ -1515,7 +1515,7 @@ const fixReversedBindingsForBindables = (
} }
: newArrow.endBinding, : newArrow.endBinding,
}); });
mutateElement(duplicate, { mutateElementWith(duplicate, originalElements, {
boundElements: [ boundElements: [
...(duplicate.boundElements ?? []).filter( ...(duplicate.boundElements ?? []).filter(
(el) => el.id !== binding.id && el.id !== newArrowId, (el) => el.id !== binding.id && el.id !== newArrowId,
@ -1529,7 +1529,7 @@ const fixReversedBindingsForBindables = (
} else { } else {
// Linked arrow is outside the selection, // Linked arrow is outside the selection,
// so we move the binding to the duplicate // so we move the binding to the duplicate
mutateElement(oldArrow, { mutateElementWith(oldArrow, originalElements, {
startBinding: startBinding:
oldArrow.startBinding?.elementId === original.id oldArrow.startBinding?.elementId === original.id
? { ? {
@ -1545,7 +1545,7 @@ const fixReversedBindingsForBindables = (
} }
: oldArrow.endBinding, : oldArrow.endBinding,
}); });
mutateElement(duplicate, { mutateElementWith(duplicate, originalElements, {
boundElements: [ boundElements: [
...(duplicate.boundElements ?? []), ...(duplicate.boundElements ?? []),
{ {
@ -1554,7 +1554,7 @@ const fixReversedBindingsForBindables = (
}, },
], ],
}); });
mutateElement(original, { mutateElementWith(original, originalElements, {
boundElements: boundElements:
original.boundElements?.filter((_, i) => i !== idx) ?? null, original.boundElements?.filter((_, i) => i !== idx) ?? null,
}); });
@ -1580,13 +1580,13 @@ const fixReversedBindingsForArrows = (
const newBindable = elementsWithClones.find( const newBindable = elementsWithClones.find(
(el) => el.id === newBindableId, (el) => el.id === newBindableId,
) as ExcalidrawBindableElement; ) as ExcalidrawBindableElement;
mutateElement(duplicate, { mutateElementWith(duplicate, originalElements, {
[bindingProp]: { [bindingProp]: {
...original[bindingProp], ...original[bindingProp],
elementId: newBindableId, elementId: newBindableId,
}, },
}); });
mutateElement(newBindable, { mutateElementWith(newBindable, originalElements, {
boundElements: [ boundElements: [
...(newBindable.boundElements ?? []).filter( ...(newBindable.boundElements ?? []).filter(
(el) => el.id !== original.id && el.id !== duplicate.id, (el) => el.id !== original.id && el.id !== duplicate.id,
@ -1603,13 +1603,13 @@ const fixReversedBindingsForArrows = (
(el) => el.id === oldBindableId, (el) => el.id === oldBindableId,
); );
if (originalBindable) { if (originalBindable) {
mutateElement(duplicate, { mutateElementWith(duplicate, originalElements, {
[bindingProp]: original[bindingProp], [bindingProp]: original[bindingProp],
}); });
mutateElement(original, { mutateElementWith(original, originalElements, {
[bindingProp]: null, [bindingProp]: null,
}); });
mutateElement(originalBindable, { mutateElementWith(originalBindable, originalElements, {
boundElements: [ boundElements: [
...(originalBindable.boundElements?.filter( ...(originalBindable.boundElements?.filter(
(el) => el.id !== original.id, (el) => el.id !== original.id,
@ -1671,8 +1671,12 @@ export const fixBindingsAfterDeletion = (
const elements = arrayToMap(sceneElements); const elements = arrayToMap(sceneElements);
for (const element of deletedElements) { for (const element of deletedElements) {
BoundElement.unbindAffected(elements, element, mutateElement); BoundElement.unbindAffected(elements, element, (element, updates) =>
BindableElement.unbindAffected(elements, element, mutateElement); mutateElementWith(element, elements, updates),
);
BindableElement.unbindAffected(elements, element, (element, updates) =>
mutateElementWith(element, elements, updates),
);
} }
}; };

View file

@ -19,7 +19,7 @@ import {
type Heading, type Heading,
} from "./heading"; } from "./heading";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement"; import { mutateElementWith } from "./mutateElement";
import { newArrowElement, newElement } from "./newElement"; import { newArrowElement, newElement } from "./newElement";
import { aabbForElement } from "./shapes"; import { aabbForElement } from "./shapes";
import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame"; import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame";
@ -678,7 +678,7 @@ export class FlowChartCreator {
) )
) { ) {
this.pendingNodes = this.pendingNodes.map((node) => this.pendingNodes = this.pendingNodes.map((node) =>
mutateElement(node, { mutateElementWith(node, elementsMap, {
frameId: startNode.frameId, frameId: startNode.frameId,
}), }),
); );

View file

@ -47,7 +47,7 @@ import {
} from "./bounds"; } from "./bounds";
import { headingIsHorizontal, vectorToHeading } from "./heading"; import { headingIsHorizontal, vectorToHeading } from "./heading";
import { mutateElementWith, mutateElement } from "./mutateElement"; import { mutateElementWith } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isBindingElement, isBindingElement,
@ -356,7 +356,7 @@ export class LinearElementEditor {
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) { if (boundTextElement) {
handleBindTextResize(element, elementsMap, false); handleBindTextResize(element, scene, false);
} }
// suggest bindings for first and last point if selected // suggest bindings for first and last point if selected
@ -1571,7 +1571,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
); );
if (points.length < 2) { if (points.length < 2) {
mutateElement(boundTextElement, { isDeleted: true }); mutateElementWith(boundTextElement, elementsMap, { isDeleted: true });
} }
let x = 0; let x = 0;
let y = 0; let y = 0;

View file

@ -962,8 +962,6 @@ export const resizeSingleElement = (
informMutation: shouldInformMutation, informMutation: shouldInformMutation,
}); });
const elementsMap = scene.getNonDeletedElementsMap();
updateBoundElements(latestElement, scene, { updateBoundElements(latestElement, scene, {
// TODO: confirm with MARK if this actually makes sense // TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight }, newSize: { width: nextWidth, height: nextHeight },
@ -976,7 +974,7 @@ export const resizeSingleElement = (
} }
handleBindTextResize( handleBindTextResize(
latestElement, latestElement,
elementsMap, scene,
handleDirection, handleDirection,
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
); );
@ -1536,7 +1534,7 @@ export const resizeMultipleElements = (
fontSize: boundTextFontSize, fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle, angle: isLinearElement(element) ? undefined : angle,
}); });
handleBindTextResize(element, elementsMap, handleDirection, true); handleBindTextResize(element, scene, handleDirection, true);
} }
} }

View file

@ -6,7 +6,6 @@ import {
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types"; import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
import { getCommonBounds, getElementBounds } from "./bounds"; import { getCommonBounds, getElementBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { isFreeDrawElement, isLinearElement } from "./typeChecks"; import { isFreeDrawElement, isLinearElement } from "./typeChecks";
import type { ElementsMap, ExcalidrawElement } from "./types"; import type { ElementsMap, ExcalidrawElement } from "./types";
@ -170,41 +169,6 @@ export const getLockedLinearCursorAlignSize = (
return { width, height }; return { width, height };
}; };
export const resizePerfectLineForNWHandler = (
element: ExcalidrawElement,
x: number,
y: number,
) => {
const anchorX = element.x + element.width;
const anchorY = element.y + element.height;
const distanceToAnchorX = x - anchorX;
const distanceToAnchorY = y - anchorY;
if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
mutateElement(element, {
x: anchorX,
width: 0,
y,
height: -distanceToAnchorY,
});
} else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
mutateElement(element, {
y: anchorY,
height: 0,
});
} else {
const nextHeight =
Math.sign(distanceToAnchorY) *
Math.sign(distanceToAnchorX) *
element.width;
mutateElement(element, {
x,
y: anchorY - nextHeight,
width: -distanceToAnchorX,
height: nextHeight,
});
}
};
export const getNormalizedDimensions = ( export const getNormalizedDimensions = (
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">, element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
): { ): {

View file

@ -18,7 +18,6 @@ import {
} from "./containerCache"; } from "./containerCache";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { measureText } from "./textMeasurements"; import { measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping"; import { wrapText } from "./textWrapping";
import { import {
@ -126,10 +125,11 @@ export const redrawTextBoundingBox = (
export const handleBindTextResize = ( export const handleBindTextResize = (
container: NonDeletedExcalidrawElement, container: NonDeletedExcalidrawElement,
elementsMap: ElementsMap, scene: Scene,
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false, shouldMaintainAspectRatio = false,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
const boundTextElementId = getBoundTextElementId(container); const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId) { if (!boundTextElementId) {
return; return;
@ -182,20 +182,20 @@ export const handleBindTextResize = (
transformHandleType === "n") transformHandleType === "n")
? container.y - diff ? container.y - diff
: container.y; : container.y;
mutateElement(container, { scene.mutate(container, {
height: containerHeight, height: containerHeight,
y: updatedY, y: updatedY,
}); });
} }
mutateElement(textElement, { scene.mutate(textElement, {
text, text,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
}); });
if (!isArrowElement(container)) { if (!isArrowElement(container)) {
mutateElement( scene.mutate(
textElement, textElement,
computeBoundTextPosition(container, textElement, elementsMap), computeBoundTextPosition(container, textElement, elementsMap),
); );

View file

@ -25,7 +25,6 @@ import {
import type { LocalPoint } from "@excalidraw/math"; import type { LocalPoint } from "@excalidraw/math";
import { mutateElement } from "../src/mutateElement";
import { duplicateElement, duplicateElements } from "../src/duplicate"; import { duplicateElement, duplicateElements } from "../src/duplicate";
import type { ExcalidrawLinearElement } from "../src/types"; import type { ExcalidrawLinearElement } from "../src/types";
@ -63,7 +62,7 @@ describe("duplicating single elements", () => {
// @ts-ignore // @ts-ignore
element.__proto__ = { hello: "world" }; element.__proto__ = { hello: "world" };
mutateElement(element, { h.app.scene.mutate(element, {
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)], points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
}); });

View file

@ -1,6 +1,6 @@
import { ARROW_TYPE } from "@excalidraw/common"; import { ARROW_TYPE } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math"; import { pointFrom } from "@excalidraw/math";
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw"; import { Excalidraw } from "@excalidraw/excalidraw";
import { actionSelectAll } from "@excalidraw/excalidraw/actions"; import { actionSelectAll } from "@excalidraw/excalidraw/actions";
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection"; import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
@ -143,7 +143,7 @@ describe("elbow arrow routing", () => {
elbowed: true, elbowed: true,
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow); scene.insertElement(arrow);
mutateElement(arrow, { h.app.scene.mutate(arrow, {
points: [ points: [
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y), pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y), pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
@ -195,7 +195,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);
mutateElement(arrow, { h.app.scene.mutate(arrow, {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)], points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
}); });

View file

@ -1,10 +1,10 @@
import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { mutateElement } from "../src/mutateElement";
import { normalizeElementOrder } from "../src/sortElements"; import { normalizeElementOrder } from "../src/sortElements";
import type { ExcalidrawElement } from "../src/types"; import type { ExcalidrawElement } from "../src/types";
const { h } = window;
const assertOrder = ( const assertOrder = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
expectedOrder: string[], expectedOrder: string[],
@ -35,7 +35,7 @@ describe("normalizeElementsOrder", () => {
boundElements: [], boundElements: [],
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ type: "text", id: boundText.id }], boundElements: [{ type: "text", id: boundText.id }],
}); });
@ -352,7 +352,7 @@ describe("normalizeElementsOrder", () => {
containerId: container.id, containerId: container.id,
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [ boundElements: [
{ type: "text", id: boundText.id }, { type: "text", id: boundText.id },
{ type: "text", id: "xxx" }, { type: "text", id: "xxx" },
@ -387,7 +387,7 @@ describe("normalizeElementsOrder", () => {
boundElements: [], boundElements: [],
groupIds: ["C", "A"], groupIds: ["C", "A"],
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ type: "text", id: boundText.id }], boundElements: [{ type: "text", id: boundText.id }],
}); });

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Excalidraw, mutateElement } from "../index"; import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { act, assertElements, render } from "../tests/test-utils"; import { act, assertElements, render } from "../tests/test-utils";
@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: f1.id, frameId: f1.id,
}); });
mutateElement(r1, { h.app.scene.mutate(r1, {
boundElements: [{ type: "text", id: t1.id }], boundElements: [{ type: "text", id: t1.id }],
}); });
@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null, frameId: null,
}); });
mutateElement(r1, { h.app.scene.mutate(r1, {
boundElements: [{ type: "text", id: t1.id }], boundElements: [{ type: "text", id: t1.id }],
}); });
@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null, frameId: null,
}); });
mutateElement(r1, { h.app.scene.mutate(r1, {
boundElements: [{ type: "text", id: t1.id }], boundElements: [{ type: "text", id: t1.id }],
}); });
@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null, frameId: null,
}); });
mutateElement(a1, { h.app.scene.mutate(a1, {
boundElements: [{ type: "text", id: t1.id }], boundElements: [{ type: "text", id: t1.id }],
}); });

View file

@ -15,7 +15,7 @@ import {
} from "@excalidraw/element/binding"; } from "@excalidraw/element/binding";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { import {
mutateElement, mutateElementWith,
newElementWith, newElementWith,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
import { import {
@ -1344,10 +1344,11 @@ export class ElementsChange implements Change<SceneElementsMap> {
updates as ElementUpdate<OrderedExcalidrawElement>, updates as ElementUpdate<OrderedExcalidrawElement>,
); );
} else { } else {
affectedElement = mutateElement( affectedElement = mutateElementWith(
nextElement, nextElement,
nextElements,
updates as ElementUpdate<OrderedExcalidrawElement>, updates as ElementUpdate<OrderedExcalidrawElement>,
); ) as OrderedExcalidrawElement;
} }
nextAffectedElements.set(affectedElement.id, affectedElement); nextAffectedElements.set(affectedElement.id, affectedElement);

View file

@ -7,7 +7,7 @@ import {
isPromiseLike, isPromiseLike,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { mutateElement } from "@excalidraw/element/mutateElement"; import { mutateElementWith } from "@excalidraw/element/mutateElement";
import { deepCopyElement } from "@excalidraw/element/duplicate"; import { deepCopyElement } from "@excalidraw/element/duplicate";
import { import {
isFrameLikeElement, isFrameLikeElement,
@ -172,7 +172,7 @@ export const serializeAsClipboardJSON = ({
!framesToCopy.has(getContainingFrame(element, elementsMap)!) !framesToCopy.has(getContainingFrame(element, elementsMap)!)
) { ) {
const copiedElement = deepCopyElement(element); const copiedElement = deepCopyElement(element);
mutateElement(copiedElement, { mutateElementWith(copiedElement, elementsMap, {
frameId: null, frameId: null,
}); });
return copiedElement; return copiedElement;

View file

@ -1686,7 +1686,7 @@ class App extends React.Component<AppProps, AppState> {
<Hyperlink <Hyperlink
key={firstSelectedElement.id} key={firstSelectedElement.id}
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={allElementsMap} scene={this.scene}
setAppState={this.setAppState} setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen} onLinkOpen={this.props.onLinkOpen}
setToast={this.setToast} setToast={this.setToast}

View file

@ -6,9 +6,10 @@ import {
defaultGetElementLinkFromSelection, defaultGetElementLinkFromSelection,
getLinkIdAndTypeFromSelection, getLinkIdAndTypeFromSelection,
} from "@excalidraw/element/elementLink"; } from "@excalidraw/element/elementLink";
import { mutateElement } from "@excalidraw/element/mutateElement";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import type { ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
import "./ElementLinkDialog.scss"; import "./ElementLinkDialog.scss";
import type { AppProps, AppState, UIAppState } from "../types"; import type { AppProps, AppState, UIAppState } from "../types";
const ElementLinkDialog = ({ const ElementLinkDialog = ({
sourceElementId, sourceElementId,
onClose, onClose,
elementsMap,
appState, appState,
scene,
generateLinkForSelection = defaultGetElementLinkFromSelection, generateLinkForSelection = defaultGetElementLinkFromSelection,
}: { }: {
sourceElementId: ExcalidrawElement["id"]; sourceElementId: ExcalidrawElement["id"];
elementsMap: ElementsMap;
appState: UIAppState; appState: UIAppState;
scene: Scene;
onClose?: () => void; onClose?: () => void;
generateLinkForSelection: AppProps["generateLinkForSelection"]; generateLinkForSelection: AppProps["generateLinkForSelection"];
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap();
const originalLink = elementsMap.get(sourceElementId)?.link ?? null; const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
const [nextLink, setNextLink] = useState<string | null>(originalLink); const [nextLink, setNextLink] = useState<string | null>(originalLink);
@ -70,7 +71,7 @@ const ElementLinkDialog = ({
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) { if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
const elementToLink = elementsMap.get(sourceElementId); const elementToLink = elementsMap.get(sourceElementId);
elementToLink && elementToLink &&
mutateElement(elementToLink, { scene.mutate(elementToLink, {
link: nextLink, link: nextLink,
}); });
} }
@ -78,13 +79,13 @@ const ElementLinkDialog = ({
if (!nextLink && linkEdited && sourceElementId) { if (!nextLink && linkEdited && sourceElementId) {
const elementToLink = elementsMap.get(sourceElementId); const elementToLink = elementsMap.get(sourceElementId);
elementToLink && elementToLink &&
mutateElement(elementToLink, { scene.mutate(elementToLink, {
link: null, link: null,
}); });
} }
onClose?.(); onClose?.();
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]); }, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -490,7 +490,7 @@ const LayerUI = ({
openDialog: null, openDialog: null,
}); });
}} }}
elementsMap={app.scene.getNonDeletedElementsMap()} scene={app.scene}
appState={appState} appState={appState}
generateLinkForSelection={generateLinkForSelection} generateLinkForSelection={generateLinkForSelection}
/> />

View file

@ -1,4 +1,3 @@
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
redrawTextBoundingBox, redrawTextBoundingBox,
@ -69,7 +68,7 @@ const handleFontSizeChange: DragInputCallbackType<
} }
if (nextFontSize) { if (nextFontSize) {
mutateElement(latestElement, { scene.mutate(latestElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });
redrawTextBoundingBox( redrawTextBoundingBox(

View file

@ -1,7 +1,5 @@
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math"; import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { getBoundTextElement } from "@excalidraw/element/textElement"; import { getBoundTextElement } from "@excalidraw/element/textElement";
import { isArrowElement } from "@excalidraw/element/typeChecks"; import { isArrowElement } from "@excalidraw/element/typeChecks";
@ -55,13 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
if (!element) { if (!element) {
continue; continue;
} }
mutateElement(element, { scene.mutate(element, {
angle: nextAngle, angle: nextAngle,
}); });
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) { if (boundTextElement && !isArrowElement(element)) {
mutateElement(boundTextElement, { angle: nextAngle }); scene.mutate(boundTextElement, { angle: nextAngle });
} }
} }
@ -89,13 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
mutateElement(latestElement, { scene.mutate(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
mutateElement(boundTextElement, { angle: nextAngle }); scene.mutate(boundTextElement, { angle: nextAngle });
} }
} }
scene.triggerUpdate(); scene.triggerUpdate();

View file

@ -99,7 +99,7 @@ const resizeElementInGroup = (
}); });
handleBindTextResize( handleBindTextResize(
latestElement, latestElement,
elementsMap, scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
true, true,
); );

View file

@ -1,4 +1,3 @@
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
redrawTextBoundingBox, redrawTextBoundingBox,
@ -85,7 +84,7 @@ const handleFontSizeChange: DragInputCallbackType<
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE); nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
for (const textElement of latestTextElements) { for (const textElement of latestTextElements) {
mutateElement(textElement, { scene.mutate(textElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });
@ -113,7 +112,7 @@ const handleFontSizeChange: DragInputCallbackType<
if (shouldChangeByStepSize) { if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
} }
mutateElement(latestElement, { scene.mutate(latestElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });

View file

@ -4,7 +4,6 @@ import {
getFlipAdjustedCropPosition, getFlipAdjustedCropPosition,
getUncroppedWidthAndHeight, getUncroppedWidthAndHeight,
} from "@excalidraw/element/cropElement"; } from "@excalidraw/element/cropElement";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { isImageElement } from "@excalidraw/element/typeChecks"; import { isImageElement } from "@excalidraw/element/typeChecks";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
@ -101,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
}; };
} }
mutateElement(element, { scene.mutate(element, {
crop: nextCrop, crop: nextCrop,
}); });
@ -119,7 +118,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height), y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
}; };
mutateElement(element, { scene.mutate(element, {
crop: nextCrop, crop: nextCrop,
}); });

View file

@ -17,7 +17,7 @@ import type {
ExcalidrawTextElement, ExcalidrawTextElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import { Excalidraw, getCommonBounds, mutateElement } from "../.."; import { Excalidraw, getCommonBounds } from "../..";
import { actionGroup } from "../../actions"; import { actionGroup } from "../../actions";
import { t } from "../../i18n"; import { t } from "../../i18n";
import * as StaticScene from "../../renderer/staticScene"; import * as StaticScene from "../../renderer/staticScene";
@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
containerId: container.id, containerId: container.id,
fontSize: 20, fontSize: 20,
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
}); });
API.setElements([container, text]); API.setElements([container, text]);

View file

@ -21,8 +21,6 @@ import {
embeddableURLValidator, embeddableURLValidator,
} from "@excalidraw/element/embeddable"; } from "@excalidraw/element/embeddable";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
sceneCoordsToViewportCoords, sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords, viewportCoordsToSceneCoords,
@ -33,6 +31,8 @@ import {
import { isEmbeddableElement } from "@excalidraw/element/typeChecks"; import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
import type Scene from "@excalidraw/element/Scene";
import type { import type {
ElementsMap, ElementsMap,
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
export const Hyperlink = ({ export const Hyperlink = ({
element, element,
elementsMap, scene,
setAppState, setAppState,
onLinkOpen, onLinkOpen,
setToast, setToast,
updateEmbedValidationStatus, updateEmbedValidationStatus,
}: { }: {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap; scene: Scene;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"]; onLinkOpen: ExcalidrawProps["onLinkOpen"];
setToast: ( setToast: (
@ -88,6 +88,7 @@ export const Hyperlink = ({
status: boolean, status: boolean,
) => void; ) => void;
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap();
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const appProps = useAppProps(); const appProps = useAppProps();
const device = useDevice(); const device = useDevice();
@ -114,7 +115,7 @@ export const Hyperlink = ({
setAppState({ activeEmbeddable: null }); setAppState({ activeEmbeddable: null });
} }
if (!link) { if (!link) {
mutateElement(element, { scene.mutate(element, {
link: null, link: null,
}); });
updateEmbedValidationStatus(element, false); updateEmbedValidationStatus(element, false);
@ -126,7 +127,7 @@ export const Hyperlink = ({
setToast({ message: t("toast.unableToEmbed"), closable: true }); setToast({ message: t("toast.unableToEmbed"), closable: true });
} }
element.link && embeddableLinkCache.set(element.id, element.link); element.link && embeddableLinkCache.set(element.id, element.link);
mutateElement(element, { scene.mutate(element, {
link, link,
}); });
updateEmbedValidationStatus(element, false); updateEmbedValidationStatus(element, false);
@ -144,7 +145,7 @@ export const Hyperlink = ({
: 1; : 1;
const hasLinkChanged = const hasLinkChanged =
embeddableLinkCache.get(element.id) !== element.link; embeddableLinkCache.get(element.id) !== element.link;
mutateElement(element, { scene.mutate(element, {
...(hasLinkChanged ...(hasLinkChanged
? { ? {
width: width:
@ -169,10 +170,11 @@ export const Hyperlink = ({
} }
} }
} else { } else {
mutateElement(element, { link }); scene.mutate(element, { link });
} }
}, [ }, [
element, element,
scene,
setToast, setToast,
appProps.validateEmbeddable, appProps.validateEmbeddable,
appState.activeEmbeddable, appState.activeEmbeddable,
@ -229,9 +231,9 @@ export const Hyperlink = ({
const handleRemove = useCallback(() => { const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete"); trackEvent("hyperlink", "delete");
mutateElement(element, { link: null }); scene.mutate(element, { link: null });
setAppState({ showHyperlinkPopup: false }); setAppState({ showHyperlinkPopup: false });
}, [setAppState, element]); }, [setAppState, element, scene]);
const onEdit = () => { const onEdit = () => {
trackEvent("hyperlink", "edit", "popup-ui"); trackEvent("hyperlink", "edit", "popup-ui");

View file

@ -1,7 +1,5 @@
import React from "react"; import React from "react";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { KEYS } from "@excalidraw/common"; import { KEYS } from "@excalidraw/common";
import { actionSelectAll } from "../actions"; import { actionSelectAll } from "../actions";
@ -298,7 +296,7 @@ describe("element locking", () => {
height: textSize, height: textSize,
containerId: container.id, containerId: container.id,
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
@ -339,7 +337,7 @@ describe("element locking", () => {
containerId: container.id, containerId: container.id,
locked: true, locked: true,
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
API.setElements([container, text]); API.setElements([container, text]);
@ -373,7 +371,7 @@ describe("element locking", () => {
containerId: container.id, containerId: container.id,
locked: true, locked: true,
}); });
mutateElement(container, { h.app.scene.mutate(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
API.setElements([container, text]); API.setElements([container, text]);

View file

@ -6,7 +6,6 @@ import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common"; import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
newArrowElement, newArrowElement,
newElement, newElement,
@ -100,10 +99,10 @@ export class API {
// eslint-disable-next-line prettier/prettier // eslint-disable-next-line prettier/prettier
static updateElement = <T extends ExcalidrawElement>( static updateElement = <T extends ExcalidrawElement>(
...args: Parameters<typeof mutateElement<T>> ...args: Parameters<typeof h.app.scene.mutate<T>>
) => { ) => {
act(() => { act(() => {
mutateElement<T>(...args); h.app.scene.mutate(...args);
}); });
}; };
@ -419,7 +418,7 @@ export class API {
}); });
mutateElement( h.app.scene.mutate(
rectangle, rectangle,
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
@ -453,7 +452,7 @@ export class API {
: opts?.label?.frameId ?? null, : opts?.label?.frameId ?? null,
}); });
mutateElement( h.app.scene.mutate(
arrow, arrow,
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],

View file

@ -5,7 +5,6 @@ import {
getElementPointsCoords, getElementPointsCoords,
} from "@excalidraw/element/bounds"; } from "@excalidraw/element/bounds";
import { cropElement } from "@excalidraw/element/cropElement"; import { cropElement } from "@excalidraw/element/cropElement";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
getTransformHandles, getTransformHandles,
getTransformHandlesFromCoords, getTransformHandlesFromCoords,
@ -519,7 +518,7 @@ export class UI {
if (angle !== 0) { if (angle !== 0) {
act(() => { act(() => {
mutateElement(origElement, { angle }); h.app.scene.mutate(origElement, { angle });
}); });
} }

View file

@ -30,7 +30,7 @@ import type {
FontString, FontString,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import { Excalidraw, mutateElement } from "../index"; import { Excalidraw } from "../index";
import * as InteractiveCanvas from "../renderer/interactiveScene"; import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene"; import * as StaticScene from "../renderer/staticScene";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
@ -118,7 +118,7 @@ describe("Test Linear Elements", () => {
], ],
roundness, roundness,
}); });
mutateElement(line, { points: line.points }); h.app.scene.mutate(line, { points: line.points });
API.setElements([line]); API.setElements([line]);
mouse.clickAt(p1[0], p1[1]); mouse.clickAt(p1[0], p1[1]);
return line; return line;

View file

@ -15,7 +15,7 @@ import {
} from "@excalidraw/element/containerCache"; } from "@excalidraw/element/containerCache";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { bumpVersion, mutateElement } from "@excalidraw/element/mutateElement"; import { bumpVersion } from "@excalidraw/element/mutateElement";
import { import {
getBoundTextElementId, getBoundTextElementId,
getContainerElement, getContainerElement,
@ -199,7 +199,7 @@ export const textWysiwyg = ({
container.type, container.type,
); );
mutateElement(container, { height: targetContainerHeight }); app.scene.mutate(container, { height: targetContainerHeight });
return; return;
} else if ( } else if (
// autoshrink container height until original container height // autoshrink container height until original container height
@ -212,7 +212,7 @@ export const textWysiwyg = ({
height, height,
container.type, container.type,
); );
mutateElement(container, { height: targetContainerHeight }); app.scene.mutate(container, { height: targetContainerHeight });
} else { } else {
const { y } = computeBoundTextPosition( const { y } = computeBoundTextPosition(
container, container,
@ -285,7 +285,7 @@ export const textWysiwyg = ({
editable.style.fontFamily = getFontFamilyString(updatedTextElement); editable.style.fontFamily = getFontFamilyString(updatedTextElement);
} }
mutateElement(updatedTextElement, { x: coordX, y: coordY }); app.scene.mutate(updatedTextElement, { x: coordX, y: coordY });
} }
}; };
@ -557,7 +557,7 @@ export const textWysiwyg = ({
if (editable.value.trim()) { if (editable.value.trim()) {
const boundTextElementId = getBoundTextElementId(container); const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) { if (!boundTextElementId || boundTextElementId !== element.id) {
mutateElement(container, { app.scene.mutate(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
type: "text", type: "text",
id: element.id, id: element.id,
@ -568,7 +568,7 @@ export const textWysiwyg = ({
bumpVersion(container); bumpVersion(container);
} }
} else { } else {
mutateElement(container, { app.scene.mutate(container, {
boundElements: container.boundElements?.filter( boundElements: container.boundElements?.filter(
(ele) => (ele) =>
!isTextElement( !isTextElement(