Expose scene.mutateElement and use original mutateElement

This commit is contained in:
Marcel Mraz 2025-04-16 13:10:14 +02:00
parent 11600ee6a6
commit 2e4ca2d11a
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
44 changed files with 249 additions and 260 deletions

View file

@ -20,7 +20,7 @@ import {
import { getSelectedElements } from "@excalidraw/element/selection"; import { getSelectedElements } from "@excalidraw/element/selection";
import { import {
mutateElementWith, mutateElement,
type ElementUpdate, type ElementUpdate,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
@ -424,23 +424,22 @@ class Scene {
return getElementsInGroup(elementsMap, id); return getElementsInGroup(elementsMap, id);
}; };
// TODO_SCENE: should be accessed as app.scene through the API
// TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead
// Mutate an element with passed updates and trigger the component to update. Make sure you // Mutate an element with passed updates and trigger the component to update. Make sure you
// are calling it either from a React event handler or within unstable_batchedUpdates(). // are calling it either from a React event handler or within unstable_batchedUpdates().
mutate<TElement extends Mutable<ExcalidrawElement>>( mutateElement<TElement extends Mutable<ExcalidrawElement>>(
element: TElement, element: TElement,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
options: { options: {
informMutation?: boolean; informMutation: boolean;
isDragging?: boolean; isDragging: boolean;
} = { } = {
informMutation: true, informMutation: true,
isDragging: false,
}, },
) { ) {
const elementsMap = this.getNonDeletedElementsMap(); const elementsMap = this.getNonDeletedElementsMap();
mutateElementWith(element, elementsMap, updates, options); mutateElement(element, elementsMap, updates, options);
if (options.informMutation) { if (options.informMutation) {
this.triggerUpdate(); this.triggerUpdate();

View file

@ -32,7 +32,7 @@ export const alignElements = (
); );
return group.map((element) => { return group.map((element) => {
// update element // update element
const updatedEle = scene.mutate(element, { const updatedEle = scene.mutateElement(element, {
x: element.x + translation.x, x: element.x + translation.x,
y: element.y + translation.y, y: element.y + translation.y,
}); });

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 } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isArrowElement, isArrowElement,
@ -157,7 +157,7 @@ export const bindOrUnbindLinearElement = (
); );
getNonDeletedElements(scene, onlyUnbound).forEach((element) => { getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
scene.mutate(element, { scene.mutateElement(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
(element) => (element) =>
element.type !== "arrow" || element.id !== linearElement.id, element.type !== "arrow" || element.id !== linearElement.id,
@ -509,13 +509,13 @@ export const bindLinearElement = (
}; };
} }
scene.mutate(linearElement, { scene.mutateElement(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
}); });
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []); const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
if (!boundElementsMap.has(linearElement.id)) { if (!boundElementsMap.has(linearElement.id)) {
scene.mutate(hoveredElement, { scene.mutateElement(hoveredElement, {
boundElements: (hoveredElement.boundElements || []).concat({ boundElements: (hoveredElement.boundElements || []).concat({
id: linearElement.id, id: linearElement.id,
type: "arrow", type: "arrow",
@ -564,7 +564,7 @@ const unbindLinearElement = (
if (binding == null) { if (binding == null) {
return null; return null;
} }
scene.mutate(linearElement, { [field]: null }); scene.mutateElement(linearElement, { [field]: null });
return binding.elementId; return binding.elementId;
}; };
@ -790,7 +790,7 @@ export const updateBoundElements = (
// `linearElement` is being moved/scaled already, just update the binding // `linearElement` is being moved/scaled already, just update the binding
if (simultaneouslyUpdatedElementIds.has(element.id)) { if (simultaneouslyUpdatedElementIds.has(element.id)) {
mutateElementWith(element, elementsMap, bindings); scene.mutateElement(element, bindings);
return; return;
} }
@ -1499,7 +1499,7 @@ const fixReversedBindingsForBindables = (
(el) => el.id === newArrowId, (el) => el.id === newArrowId,
)! as ExcalidrawArrowElement; )! as ExcalidrawArrowElement;
mutateElementWith(newArrow, originalElements, { mutateElement(newArrow, originalElements, {
startBinding: startBinding:
oldArrow.startBinding?.elementId === binding.id oldArrow.startBinding?.elementId === binding.id
? { ? {
@ -1515,7 +1515,7 @@ const fixReversedBindingsForBindables = (
} }
: newArrow.endBinding, : newArrow.endBinding,
}); });
mutateElementWith(duplicate, originalElements, { mutateElement(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
mutateElementWith(oldArrow, originalElements, { mutateElement(oldArrow, originalElements, {
startBinding: startBinding:
oldArrow.startBinding?.elementId === original.id oldArrow.startBinding?.elementId === original.id
? { ? {
@ -1545,7 +1545,7 @@ const fixReversedBindingsForBindables = (
} }
: oldArrow.endBinding, : oldArrow.endBinding,
}); });
mutateElementWith(duplicate, originalElements, { mutateElement(duplicate, originalElements, {
boundElements: [ boundElements: [
...(duplicate.boundElements ?? []), ...(duplicate.boundElements ?? []),
{ {
@ -1554,7 +1554,7 @@ const fixReversedBindingsForBindables = (
}, },
], ],
}); });
mutateElementWith(original, originalElements, { mutateElement(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;
mutateElementWith(duplicate, originalElements, { mutateElement(duplicate, originalElements, {
[bindingProp]: { [bindingProp]: {
...original[bindingProp], ...original[bindingProp],
elementId: newBindableId, elementId: newBindableId,
}, },
}); });
mutateElementWith(newBindable, originalElements, { mutateElement(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) {
mutateElementWith(duplicate, originalElements, { mutateElement(duplicate, originalElements, {
[bindingProp]: original[bindingProp], [bindingProp]: original[bindingProp],
}); });
mutateElementWith(original, originalElements, { mutateElement(original, originalElements, {
[bindingProp]: null, [bindingProp]: null,
}); });
mutateElementWith(originalBindable, originalElements, { mutateElement(originalBindable, originalElements, {
boundElements: [ boundElements: [
...(originalBindable.boundElements?.filter( ...(originalBindable.boundElements?.filter(
(el) => el.id !== original.id, (el) => el.id !== original.id,
@ -1672,10 +1672,10 @@ export const fixBindingsAfterDeletion = (
for (const element of deletedElements) { for (const element of deletedElements) {
BoundElement.unbindAffected(elements, element, (element, updates) => BoundElement.unbindAffected(elements, element, (element, updates) =>
mutateElementWith(element, elements, updates), mutateElement(element, elements, updates),
); );
BindableElement.unbindAffected(elements, element, (element, updates) => BindableElement.unbindAffected(elements, element, (element, updates) =>
mutateElementWith(element, elements, updates), mutateElement(element, elements, updates),
); );
} }
}; };

View file

@ -168,7 +168,7 @@ const updateElementCoords = (
const nextX = originalElement.x + dragOffset.x; const nextX = originalElement.x + dragOffset.x;
const nextY = originalElement.y + dragOffset.y; const nextY = originalElement.y + dragOffset.y;
scene.mutate(element, { scene.mutateElement(element, {
x: nextX, x: nextX,
y: nextY, y: nextY,
}); });
@ -292,7 +292,7 @@ export const dragNewElement = ({
}; };
} }
scene.mutate( scene.mutateElement(
newElement, newElement,
{ {
x: newX + (originOffset?.x ?? 0), x: newX + (originOffset?.x ?? 0),
@ -302,7 +302,7 @@ export const dragNewElement = ({
...textAutoResize, ...textAutoResize,
...imageInitialDimension, ...imageInitialDimension,
}, },
{ informMutation }, { informMutation, isDragging: false },
); );
} }
}; };

View file

@ -46,7 +46,7 @@ import {
headingForPoint, headingForPoint,
} from "./heading"; } from "./heading";
import { type ElementUpdate } from "./mutateElement"; import { type ElementUpdate } from "./mutateElement";
import { isBindableElement, isElbowArrow } from "./typeChecks"; import { isBindableElement } from "./typeChecks";
import { import {
type ExcalidrawElbowArrowElement, type ExcalidrawElbowArrowElement,
type NonDeletedSceneElementsMap, type NonDeletedSceneElementsMap,
@ -60,7 +60,6 @@ import type {
Arrowhead, Arrowhead,
ElementsMap, ElementsMap,
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement,
FixedPointBinding, FixedPointBinding,
FixedSegment, FixedSegment,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
@ -879,27 +878,6 @@ const handleEndpointDrag = (
const MAX_POS = 1e6; const MAX_POS = 1e6;
export const elbowArrowNeedsToGetNormalized = (
element: Readonly<ExcalidrawElement>,
updates: {
points?: readonly LocalPoint[];
fixedSegments?: readonly FixedSegment[] | null;
startBinding?: FixedPointBinding | null;
endBinding?: FixedPointBinding | null;
},
) => {
const { points, fixedSegments, startBinding, endBinding } = updates;
return (
isElbowArrow(element) &&
(Object.keys(updates).length === 0 || // normalization case
typeof points !== "undefined" || // repositioning
typeof fixedSegments !== "undefined" || // segment fixing
typeof startBinding !== "undefined" ||
typeof endBinding !== "undefined") // manual binding to element
);
};
/** /**
* *
*/ */

View file

@ -19,7 +19,7 @@ import {
type Heading, type Heading,
} from "./heading"; } from "./heading";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElementWith } from "./mutateElement"; import { mutateElement } 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) =>
mutateElementWith(node, elementsMap, { mutateElement(node, elementsMap, {
frameId: startNode.frameId, frameId: startNode.frameId,
}), }),
); );

View file

@ -2,7 +2,7 @@ import { generateNKeysBetween } from "fractional-indexing";
import { arrayToMap } from "@excalidraw/common"; import { arrayToMap } from "@excalidraw/common";
import { mutateElementWith } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { hasBoundTextElement } from "./typeChecks"; import { hasBoundTextElement } from "./typeChecks";
@ -177,7 +177,7 @@ export const syncMovedIndices = (
// split mutation so we don't end up in an incosistent state // split mutation so we don't end up in an incosistent state
for (const [element, update] of elementsUpdates) { for (const [element, update] of elementsUpdates) {
mutateElementWith(element, arrayToMap(elements), update); mutateElement(element, arrayToMap(elements), update);
} }
} catch (e) { } catch (e) {
// fallback to default sync // fallback to default sync
@ -198,7 +198,7 @@ export const syncInvalidIndices = (
const indicesGroups = getInvalidIndicesGroups(elements); const indicesGroups = getInvalidIndicesGroups(elements);
const elementsUpdates = generateIndices(elements, indicesGroups); const elementsUpdates = generateIndices(elements, indicesGroups);
for (const [element, update] of elementsUpdates) { for (const [element, update] of elementsUpdates) {
mutateElementWith(element, arrayToMap(elements), update); mutateElement(element, arrayToMap(elements), update);
} }
return elements as OrderedExcalidrawElement[]; return elements as OrderedExcalidrawElement[];

View file

@ -19,7 +19,7 @@ import {
getCommonBounds, getCommonBounds,
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "./bounds"; } from "./bounds";
import { mutateElementWith } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getBoundTextElement, getContainerElement } from "./textElement"; import { getBoundTextElement, getContainerElement } from "./textElement";
import { import {
isFrameElement, isFrameElement,
@ -57,7 +57,7 @@ export const bindElementsToFramesAfterDuplication = (
if (nextElementId) { if (nextElementId) {
const nextElement = nextElementMap.get(nextElementId); const nextElement = nextElementMap.get(nextElementId);
if (nextElement) { if (nextElement) {
mutateElementWith(nextElement, nextElementMap, { mutateElement(nextElement, nextElementMap, {
frameId: nextFrameId ?? element.frameId, frameId: nextFrameId ?? element.frameId,
}); });
} }
@ -563,7 +563,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
} }
for (const element of finalElementsToAdd) { for (const element of finalElementsToAdd) {
mutateElementWith(element, elementsMap, { mutateElement(element, elementsMap, {
frameId: frame.id, frameId: frame.id,
}); });
} }
@ -603,7 +603,7 @@ export const removeElementsFromFrame = (
} }
for (const [, element] of _elementsToRemove) { for (const [, element] of _elementsToRemove) {
mutateElementWith(element, elementsMap, { mutateElement(element, elementsMap, {
frameId: null, frameId: null,
}); });
} }

View file

@ -47,7 +47,7 @@ import {
} from "./bounds"; } from "./bounds";
import { headingIsHorizontal, vectorToHeading } from "./heading"; import { headingIsHorizontal, vectorToHeading } from "./heading";
import { mutateElementWith } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isBindingElement, isBindingElement,
@ -793,7 +793,7 @@ export class LinearElementEditor {
); );
} else if (event.altKey && appState.editingLinearElement) { } else if (event.altKey && appState.editingLinearElement) {
if (linearElementEditor.lastUncommittedPoint == null) { if (linearElementEditor.lastUncommittedPoint == null) {
scene.mutate(element, { scene.mutateElement(element, {
points: [ points: [
...element.points, ...element.points,
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
@ -1165,7 +1165,7 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) { ) {
mutateElementWith( mutateElement(
element, element,
elementsMap, elementsMap,
LinearElementEditor.getNormalizedPoints(element), LinearElementEditor.getNormalizedPoints(element),
@ -1221,7 +1221,7 @@ export class LinearElementEditor {
return acc; return acc;
}, []); }, []);
scene.mutate(element, { points: nextPoints }); scene.mutateElement(element, { points: nextPoints });
// temp hack to ensure the line doesn't move when adding point to the end, // temp hack to ensure the line doesn't move when adding point to the end,
// potentially expanding the bounding box // potentially expanding the bounding box
@ -1442,7 +1442,7 @@ export class LinearElementEditor {
...element.points.slice(segmentMidpoint.index!), ...element.points.slice(segmentMidpoint.index!),
]; ];
scene.mutate(element, { points }); scene.mutateElement(element, { points });
ret.pointerDownState = { ret.pointerDownState = {
...linearElementEditor.pointerDownState, ...linearElementEditor.pointerDownState,
@ -1495,8 +1495,9 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints); updates.points = Array.from(nextPoints);
scene.mutate(element, updates, { scene.mutateElement(element, updates, {
isDragging: options?.isDragging, informMutation: true,
isDragging: options?.isDragging ?? false,
}); });
} else { } else {
const nextCoords = getElementPointsCoords(element, nextPoints); const nextCoords = getElementPointsCoords(element, nextPoints);
@ -1512,7 +1513,7 @@ export class LinearElementEditor {
pointFrom(dX, dY), pointFrom(dX, dY),
element.angle, element.angle,
); );
scene.mutate(element, { scene.mutateElement(element, {
...otherUpdates, ...otherUpdates,
points: nextPoints, points: nextPoints,
x: element.x + rotated[0], x: element.x + rotated[0],
@ -1571,7 +1572,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
); );
if (points.length < 2) { if (points.length < 2) {
mutateElementWith(boundTextElement, elementsMap, { isDeleted: true }); mutateElement(boundTextElement, elementsMap, { isDeleted: true });
} }
let x = 0; let x = 0;
let y = 0; let y = 0;
@ -1823,7 +1824,7 @@ export class LinearElementEditor {
.map((segment) => segment.index) .map((segment) => segment.index)
.reduce((count, idx) => (idx < index ? count + 1 : count), 0); .reduce((count, idx) => (idx < index ? count + 1 : count), 0);
scene.mutate(element, { scene.mutateElement(element, {
fixedSegments: nextFixedSegments, fixedSegments: nextFixedSegments,
}); });
@ -1860,7 +1861,7 @@ export class LinearElementEditor {
scene: Scene, scene: Scene,
index: number, index: number,
): void { ): void {
scene.mutate(element, { scene.mutateElement(element, {
fixedSegments: element.fixedSegments?.filter( fixedSegments: element.fixedSegments?.filter(
(segment) => segment.index !== index, (segment) => segment.index !== index,
), ),

View file

@ -10,12 +10,12 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { ShapeCache } from "./ShapeCache"; import { ShapeCache } from "./ShapeCache";
import { import { updateElbowArrowPoints } from "./elbowArrow";
elbowArrowNeedsToGetNormalized,
updateElbowArrowPoints, import { isElbowArrow } from "./typeChecks";
} from "./elbowArrow";
import type { import type {
ElementsMap,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
ExcalidrawElement, ExcalidrawElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
@ -26,24 +26,38 @@ export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
"id" | "version" | "versionNonce" | "updated" "id" | "version" | "versionNonce" | "updated"
>; >;
// This function tracks updates of text elements for the purposes for collaboration. /**
// The version is used to compare updates when more than one user is working in * This function tracks updates of text elements for the purposes for collaboration.
// the same drawing. Note: this won't trigger the component to update, unlike `scene.mutate`. * The version is used to compare updates when more than one user is working in
export const mutateElementWith = <TElement extends Mutable<ExcalidrawElement>>( * the same drawing.
*
* WARNING: this won't trigger the component to update, so if you need to trigger component update,
* use `scene.mutateElement` or `ExcalidrawImperativeAPI.mutateElement` instead.
*/
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement, element: TElement,
elementsMap: Map<string, ExcalidrawElement>, elementsMap: ElementsMap,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
}, },
) => { ) => {
let didChange = false;
// casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fixedSegments, startBinding, endBinding, fileId } =
updates as any;
if ( if (
elbowArrowNeedsToGetNormalized( isElbowArrow(element) &&
element, (Object.keys(updates).length === 0 || // normalization case
updates as ElementUpdate<ExcalidrawElbowArrowElement>, typeof points !== "undefined" || // repositioning
) typeof fixedSegments !== "undefined" || // segment fixing
typeof startBinding !== "undefined" ||
typeof endBinding !== "undefined") // manual binding to element
) { ) {
const normalizedUpdates = { updates = {
...updates, ...updates,
angle: 0 as Radians, angle: 0 as Radians,
...updateElbowArrowPoints( ...updateElbowArrowPoints(
@ -52,35 +66,8 @@ export const mutateElementWith = <TElement extends Mutable<ExcalidrawElement>>(
updates as ElementUpdate<ExcalidrawElbowArrowElement>, updates as ElementUpdate<ExcalidrawElbowArrowElement>,
options, options,
), ),
} as ElementUpdate<ExcalidrawElbowArrowElement>; };
} else if (typeof points !== "undefined") {
return mutateElement(
element as ExcalidrawElbowArrowElement,
normalizedUpdates,
);
}
return mutateElement(element, updates);
};
/**
* This function tracks updates of text elements for the purposes for collaboration.
* The version is used to compare updates when more than one user is working in
* the same drawing.
*
* @deprecated Use `scene.mutate` as direct equivalent, or `mutateElementWith` in case you don't need to trigger component update.
*/
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
): TElement => {
let didChange = false;
// casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fileId } = updates as any;
if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates }; updates = { ...getSizeFromPoints(points), ...updates };
} }

View file

@ -30,7 +30,6 @@ import {
getElementBounds, getElementBounds,
} from "./bounds"; } from "./bounds";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElementWith } from "./mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
getBoundTextElementId, getBoundTextElementId,
@ -230,13 +229,13 @@ const rotateSingleElement = (
} }
const boundTextElementId = getBoundTextElementId(element); const boundTextElementId = getBoundTextElementId(element);
scene.mutate(element, { angle }); scene.mutateElement(element, { angle });
if (boundTextElementId) { if (boundTextElementId) {
const textElement = const textElement =
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId); scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
if (textElement && !isArrowElement(element)) { if (textElement && !isArrowElement(element)) {
scene.mutate(textElement, { angle }); scene.mutateElement(textElement, { angle });
} }
} }
}; };
@ -391,7 +390,7 @@ const resizeSingleTextElement = (
); );
const [nextX, nextY] = newTopLeft; const [nextX, nextY] = newTopLeft;
scene.mutate(element, { scene.mutateElement(element, {
fontSize: metrics.size, fontSize: metrics.size,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
@ -506,7 +505,7 @@ const resizeSingleTextElement = (
autoResize: false, autoResize: false,
}; };
scene.mutate(element, resizedElement); scene.mutateElement(element, resizedElement);
} }
}; };
@ -552,7 +551,7 @@ const rotateMultipleElements = (
angle: normalizeRadians((centerAngle + origAngle) as Radians), angle: normalizeRadians((centerAngle + origAngle) as Radians),
}; };
scene.mutate(element, updates); scene.mutateElement(element, updates);
updateBoundElements(element, scene, { updateBoundElements(element, scene, {
simultaneouslyUpdated: elements, simultaneouslyUpdated: elements,
@ -560,7 +559,7 @@ const rotateMultipleElements = (
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) { if (boundText && !isArrowElement(element)) {
mutateElementWith(boundText, elementsMap, { scene.mutateElement(boundText, {
x: boundText.x + (rotatedCX - cx), x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy), y: boundText.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians), angle: normalizeRadians((centerAngle + origAngle) as Radians),
@ -923,7 +922,7 @@ export const resizeSingleElement = (
} }
if ("scale" in latestElement && "scale" in origElement) { if ("scale" in latestElement && "scale" in origElement) {
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
scale: [ scale: [
// defaulting because scaleX/Y can be 0/-0 // defaulting because scaleX/Y can be 0/-0
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0], (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
@ -958,8 +957,9 @@ export const resizeSingleElement = (
...rescaledPoints, ...rescaledPoints,
}; };
scene.mutate(latestElement, updates, { scene.mutateElement(latestElement, updates, {
informMutation: shouldInformMutation, informMutation: shouldInformMutation,
isDragging: false,
}); });
updateBoundElements(latestElement, scene, { updateBoundElements(latestElement, scene, {
@ -968,7 +968,7 @@ export const resizeSingleElement = (
}); });
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
scene.mutate(boundTextElement, { scene.mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize, fontSize: boundTextFont.fontSize,
}); });
} }
@ -1518,7 +1518,8 @@ export const resizeMultipleElements = (
} of elementsAndUpdates) { } of elementsAndUpdates) {
const { width, height, angle } = update; const { width, height, angle } = update;
scene.mutate(element, update, { scene.mutateElement(element, update, {
informMutation: true,
// needed for the fixed binding point udpate to take effect // needed for the fixed binding point udpate to take effect
isDragging: true, isDragging: true,
}); });
@ -1530,7 +1531,7 @@ export const resizeMultipleElements = (
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) { if (boundTextElement && boundTextFontSize) {
scene.mutate(boundTextElement, { scene.mutateElement(boundTextElement, {
fontSize: boundTextFontSize, fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle, angle: isLinearElement(element) ? undefined : angle,
}); });

View file

@ -93,7 +93,7 @@ export const redrawTextBoundingBox = (
metrics.height, metrics.height,
container.type, container.type,
); );
scene.mutate(container, { height: nextHeight }); scene.mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
} }
@ -102,7 +102,7 @@ export const redrawTextBoundingBox = (
metrics.width, metrics.width,
container.type, container.type,
); );
scene.mutate(container, { width: nextWidth }); scene.mutateElement(container, { width: nextWidth });
} }
const updatedTextElement = { const updatedTextElement = {
@ -120,7 +120,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.y = y; boundTextUpdates.y = y;
} }
scene.mutate(textElement, boundTextUpdates); scene.mutateElement(textElement, boundTextUpdates);
}; };
export const handleBindTextResize = ( export const handleBindTextResize = (
@ -182,20 +182,20 @@ export const handleBindTextResize = (
transformHandleType === "n") transformHandleType === "n")
? container.y - diff ? container.y - diff
: container.y; : container.y;
scene.mutate(container, { scene.mutateElement(container, {
height: containerHeight, height: containerHeight,
y: updatedY, y: updatedY,
}); });
} }
scene.mutate(textElement, { scene.mutateElement(textElement, {
text, text,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
}); });
if (!isArrowElement(container)) { if (!isArrowElement(container)) {
scene.mutate( scene.mutateElement(
textElement, textElement,
computeBoundTextPosition(container, textElement, elementsMap), computeBoundTextPosition(container, textElement, elementsMap),
); );

View file

@ -62,7 +62,7 @@ describe("duplicating single elements", () => {
// @ts-ignore // @ts-ignore
element.__proto__ = { hello: "world" }; element.__proto__ = { hello: "world" };
h.app.scene.mutate(element, { h.app.scene.mutateElement(element, {
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)], points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
}); });

View file

@ -143,7 +143,7 @@ describe("elbow arrow routing", () => {
elbowed: true, elbowed: true,
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow); scene.insertElement(arrow);
h.app.scene.mutate(arrow, { h.app.scene.mutateElement(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);
h.app.scene.mutate(arrow, { h.app.scene.mutateElement(arrow, {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)], points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
}); });

View file

@ -35,7 +35,7 @@ describe("normalizeElementsOrder", () => {
boundElements: [], boundElements: [],
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(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,
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(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"],
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }], boundElements: [{ type: "text", id: boundText.id }],
}); });

View file

@ -76,7 +76,7 @@ export const actionUnbindText = register({
boundTextElement, boundTextElement,
elementsMap, elementsMap,
); );
app.scene.mutate(boundTextElement as ExcalidrawTextElement, { app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null, containerId: null,
width, width,
height, height,
@ -84,7 +84,7 @@ export const actionUnbindText = register({
x, x,
y, y,
}); });
app.scene.mutate(element, { app.scene.mutateElement(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id, (ele) => ele.id !== boundTextElement.id,
), ),
@ -149,13 +149,13 @@ export const actionBindText = register({
textElement = selectedElements[1] as ExcalidrawTextElement; textElement = selectedElements[1] as ExcalidrawTextElement;
container = selectedElements[0] as ExcalidrawTextContainer; container = selectedElements[0] as ExcalidrawTextContainer;
} }
app.scene.mutate(textElement, { app.scene.mutateElement(textElement, {
containerId: container.id, containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER, textAlign: TEXT_ALIGN.CENTER,
autoResize: true, autoResize: true,
}); });
app.scene.mutate(container, { app.scene.mutateElement(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
type: "text", type: "text",
id: textElement.id, id: textElement.id,
@ -292,7 +292,7 @@ export const actionWrapTextInContainer = register({
} }
if (startBinding || endBinding) { if (startBinding || endBinding) {
app.scene.mutate(ele, { app.scene.mutateElement(ele, {
startBinding, startBinding,
endBinding, endBinding,
}); });
@ -300,7 +300,7 @@ export const actionWrapTextInContainer = register({
}); });
} }
app.scene.mutate(textElement, { app.scene.mutateElement(textElement, {
containerId: container.id, containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null, boundElements: null,

View file

@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: f1.id, frameId: f1.id,
}); });
h.app.scene.mutate(r1, { h.app.scene.mutateElement(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,
}); });
h.app.scene.mutate(r1, { h.app.scene.mutateElement(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,
}); });
h.app.scene.mutate(r1, { h.app.scene.mutateElement(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,
}); });
h.app.scene.mutate(a1, { h.app.scene.mutateElement(a1, {
boundElements: [{ type: "text", id: t1.id }], boundElements: [{ type: "text", id: t1.id }],
}); });

View file

@ -91,7 +91,7 @@ const deleteSelectedElements = (
el.boundElements.forEach((candidate) => { el.boundElements.forEach((candidate) => {
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id); const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
if (bound && isElbowArrow(bound)) { if (bound && isElbowArrow(bound)) {
app.scene.mutate(bound, { app.scene.mutateElement(bound, {
startBinding: startBinding:
el.id === bound.startBinding?.elementId el.id === bound.startBinding?.elementId
? null ? null

View file

@ -71,10 +71,10 @@ export const actionFinalize = register({
scene.getElement(appState.pendingImageElementId); scene.getElement(appState.pendingImageElementId);
if (pendingImageElement) { if (pendingImageElement) {
scene.mutate( scene.mutateElement(
pendingImageElement, pendingImageElement,
{ isDeleted: true }, { isDeleted: true },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} }
@ -99,7 +99,7 @@ export const actionFinalize = register({
!lastCommittedPoint || !lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint points[points.length - 1] !== lastCommittedPoint
) { ) {
scene.mutate(multiPointElement, { scene.mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1), points: multiPointElement.points.slice(0, -1),
}); });
} }
@ -123,7 +123,7 @@ export const actionFinalize = register({
if (isLoop) { if (isLoop) {
const linePoints = multiPointElement.points; const linePoints = multiPointElement.points;
const firstPoint = linePoints[0]; const firstPoint = linePoints[0];
scene.mutate(multiPointElement, { scene.mutateElement(multiPointElement, {
points: linePoints.map((p, index) => points: linePoints.map((p, index) =>
index === linePoints.length - 1 index === linePoints.length - 1
? pointFrom(firstPoint[0], firstPoint[1]) ? pointFrom(firstPoint[0], firstPoint[1])

View file

@ -189,13 +189,13 @@ const flipElements = (
getCommonBoundingBox(selectedElements); getCommonBoundingBox(selectedElements);
const [diffX, diffY] = [midX - newMidX, midY - newMidY]; const [diffX, diffY] = [midX - newMidX, midY - newMidY];
otherElements.forEach((element) => otherElements.forEach((element) =>
app.scene.mutate(element, { app.scene.mutateElement(element, {
x: element.x + diffX, x: element.x + diffX,
y: element.y + diffY, y: element.y + diffY,
}), }),
); );
elbowArrows.forEach((element) => elbowArrows.forEach((element) =>
app.scene.mutate(element, { app.scene.mutateElement(element, {
x: element.x + diffX, x: element.x + diffX,
y: element.y + diffY, y: element.y + diffY,
}), }),

View file

@ -1,5 +1,5 @@
import { getNonDeletedElements } from "@excalidraw/element"; import { getNonDeletedElements } from "@excalidraw/element";
import { mutateElementWith } from "@excalidraw/element/mutateElement"; import { mutateElement } from "@excalidraw/element/mutateElement";
import { newFrameElement } from "@excalidraw/element/newElement"; import { newFrameElement } from "@excalidraw/element/newElement";
import { isFrameLikeElement } from "@excalidraw/element/typeChecks"; import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
import { import {
@ -194,7 +194,7 @@ export const actionWrapSelectionInFrame = register({
for (const elementInGroup of elementsInGroup) { for (const elementInGroup of elementsInGroup) {
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId); const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
mutateElementWith(elementInGroup, elementsMap, { mutateElement(elementInGroup, elementsMap, {
groupIds: elementInGroup.groupIds.slice(0, index), groupIds: elementInGroup.groupIds.slice(0, index),
}); });
} }

View file

@ -211,7 +211,7 @@ const offsetElementAfterFontResize = (
if (isBoundToContainer(nextElement) || !nextElement.autoResize) { if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
return nextElement; return nextElement;
} }
return scene.mutate(nextElement, { return scene.mutateElement(nextElement, {
x: x:
prevElement.textAlign === "left" prevElement.textAlign === "left"
? prevElement.x ? prevElement.x
@ -915,7 +915,7 @@ export const actionChangeFontFamily = register({
if (resetContainers && container && cachedContainer) { if (resetContainers && container && cachedContainer) {
// reset the container back to it's cached version // reset the container back to it's cached version
app.scene.mutate(container, { ...cachedContainer }); app.scene.mutateElement(container, { ...cachedContainer });
} }
if (!skipFontFaceCheck) { if (!skipFontFaceCheck) {

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 {
mutateElementWith, mutateElement,
newElementWith, newElementWith,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
import { import {
@ -1344,7 +1344,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
updates as ElementUpdate<OrderedExcalidrawElement>, updates as ElementUpdate<OrderedExcalidrawElement>,
); );
} else { } else {
affectedElement = mutateElementWith( affectedElement = mutateElement(
nextElement, nextElement,
nextElements, nextElements,
updates as ElementUpdate<OrderedExcalidrawElement>, updates as ElementUpdate<OrderedExcalidrawElement>,

View file

@ -7,7 +7,7 @@ import {
isPromiseLike, isPromiseLike,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { mutateElementWith } from "@excalidraw/element/mutateElement"; import { mutateElement } 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);
mutateElementWith(copiedElement, elementsMap, { mutateElement(copiedElement, elementsMap, {
frameId: null, frameId: null,
}); });
return copiedElement; return copiedElement;

View file

@ -301,6 +301,8 @@ import { isNonDeletedElement } from "@excalidraw/element";
import Scene from "@excalidraw/element/Scene"; import Scene from "@excalidraw/element/Scene";
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
import type { LocalPoint, Radians } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math";
import type { import type {
@ -329,7 +331,7 @@ import type {
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { ValueOf } from "@excalidraw/common/utility-types"; import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
import { import {
actionAddToLibrary, actionAddToLibrary,
@ -776,6 +778,7 @@ class App extends React.Component<AppProps, AppState> {
if (excalidrawAPI) { if (excalidrawAPI) {
const api: ExcalidrawImperativeAPI = { const api: ExcalidrawImperativeAPI = {
updateScene: this.updateScene, updateScene: this.updateScene,
mutateElement: this.mutateElement,
updateLibrary: this.library.updateLibrary, updateLibrary: this.library.updateLibrary,
addFiles: this.addFiles, addFiles: this.addFiles,
resetScene: this.resetScene, resetScene: this.resetScene,
@ -1403,7 +1406,7 @@ class App extends React.Component<AppProps, AppState> {
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => { private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
if (frame) { if (frame) {
this.scene.mutate(frame, { name: frame.name?.trim() || null }); this.scene.mutateElement(frame, { name: frame.name?.trim() || null });
} }
this.setState({ editingFrame: null }); this.setState({ editingFrame: null });
}; };
@ -1460,7 +1463,7 @@ class App extends React.Component<AppProps, AppState> {
autoFocus autoFocus
value={frameNameInEdit} value={frameNameInEdit}
onChange={(e) => { onChange={(e) => {
this.scene.mutate(f, { this.scene.mutateElement(f, {
name: e.target.value, name: e.target.value,
}); });
}} }}
@ -1951,20 +1954,20 @@ class App extends React.Component<AppProps, AppState> {
// state only. // state only.
// Thus reset so that we prefer local cache (if there was some // Thus reset so that we prefer local cache (if there was some
// generationData set previously) // generationData set previously)
this.scene.mutate( this.scene.mutateElement(
frameElement, frameElement,
{ {
customData: { generationData: undefined }, customData: { generationData: undefined },
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} else { } else {
this.scene.mutate( this.scene.mutateElement(
frameElement, frameElement,
{ {
customData: { generationData: data }, customData: { generationData: data },
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} }
this.magicGenerations.set(frameElement.id, data); this.magicGenerations.set(frameElement.id, data);
@ -2136,7 +2139,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.insertElement(frame); this.scene.insertElement(frame);
for (const child of selectedElements) { for (const child of selectedElements) {
this.scene.mutate(child, { frameId: frame.id }); this.scene.mutateElement(child, { frameId: frame.id });
} }
this.setState({ this.setState({
@ -3456,10 +3459,10 @@ class App extends React.Component<AppProps, AppState> {
} }
// hack to reset the `y` coord because we vertically center during // hack to reset the `y` coord because we vertically center during
// insertImageElement // insertImageElement
this.scene.mutate( this.scene.mutateElement(
initializedImageElement, initializedImageElement,
{ y }, { y },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
y = imageElement.y + imageElement.height + 25; y = imageElement.y + imageElement.height + 25;
@ -4014,6 +4017,17 @@ class App extends React.Component<AppProps, AppState> {
}, },
); );
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
informMutation = true,
) => {
return this.scene.mutateElement(element, updates, {
informMutation,
isDragging: false,
});
};
private triggerRender = ( private triggerRender = (
/** force always re-renders canvas even if no change */ /** force always re-renders canvas even if no change */
force?: boolean, force?: boolean,
@ -4426,13 +4440,13 @@ class App extends React.Component<AppProps, AppState> {
} }
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
this.scene.mutate( this.scene.mutateElement(
element, element,
{ {
x: element.x + offsetX, x: element.x + offsetX,
y: element.y + offsetY, y: element.y + offsetY,
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
updateBoundElements(element, this.scene, { updateBoundElements(element, this.scene, {
@ -5335,7 +5349,7 @@ class App extends React.Component<AppProps, AppState> {
const minHeight = getApproxMinLineHeight(fontSize, lineHeight); const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const newHeight = Math.max(container.height, minHeight); const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth); const newWidth = Math.max(container.width, minWidth);
this.scene.mutate(container, { this.scene.mutateElement(container, {
height: newHeight, height: newHeight,
width: newWidth, width: newWidth,
}); });
@ -5389,7 +5403,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
if (!existingTextElement && shouldBindToContainer && container) { if (!existingTextElement && shouldBindToContainer && container) {
this.scene.mutate(container, { this.scene.mutateElement(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
type: "text", type: "text",
id: element.id, id: element.id,
@ -5940,7 +5954,7 @@ class App extends React.Component<AppProps, AppState> {
lastPoint, lastPoint,
) >= LINE_CONFIRM_THRESHOLD ) >= LINE_CONFIRM_THRESHOLD
) { ) {
this.scene.mutate( this.scene.mutateElement(
multiElement, multiElement,
{ {
points: [ points: [
@ -5948,7 +5962,7 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry), pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
], ],
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} else { } else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
@ -5964,12 +5978,12 @@ class App extends React.Component<AppProps, AppState> {
) < LINE_CONFIRM_THRESHOLD ) < LINE_CONFIRM_THRESHOLD
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
this.scene.mutate( this.scene.mutateElement(
multiElement, multiElement,
{ {
points: points.slice(0, -1), points: points.slice(0, -1),
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} else { } else {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
@ -6003,7 +6017,7 @@ class App extends React.Component<AppProps, AppState> {
} }
// update last uncommitted point // update last uncommitted point
this.scene.mutate( this.scene.mutateElement(
multiElement, multiElement,
{ {
points: [ points: [
@ -6688,7 +6702,7 @@ class App extends React.Component<AppProps, AppState> {
const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
this.scene.mutate(pendingImageElement, { this.scene.mutateElement(pendingImageElement, {
x, x,
y, y,
frameId: frame ? frame.id : null, frameId: frame ? frame.id : null,
@ -7742,7 +7756,7 @@ class App extends React.Component<AppProps, AppState> {
multiElement.type === "line" && multiElement.type === "line" &&
isPathALoop(multiElement.points, this.state.zoom.value) isPathALoop(multiElement.points, this.state.zoom.value)
) { ) {
this.scene.mutate(multiElement, { this.scene.mutateElement(multiElement, {
lastCommittedPoint: lastCommittedPoint:
multiElement.points[multiElement.points.length - 1], multiElement.points[multiElement.points.length - 1],
}); });
@ -7753,7 +7767,7 @@ class App extends React.Component<AppProps, AppState> {
// Elbow arrows cannot be created by putting down points // Elbow arrows cannot be created by putting down points
// only the start and end points can be defined // only the start and end points can be defined
if (isElbowArrow(multiElement) && multiElement.points.length > 1) { if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
this.scene.mutate(multiElement, { this.scene.mutateElement(multiElement, {
lastCommittedPoint: lastCommittedPoint:
multiElement.points[multiElement.points.length - 1], multiElement.points[multiElement.points.length - 1],
}); });
@ -7790,7 +7804,7 @@ class App extends React.Component<AppProps, AppState> {
})); }));
// clicking outside commit zone → update reference for last committed // clicking outside commit zone → update reference for last committed
// point // point
this.scene.mutate(multiElement, { this.scene.mutateElement(multiElement, {
lastCommittedPoint: multiElement.points[multiElement.points.length - 1], lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
}); });
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
@ -7876,7 +7890,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
}; };
}); });
this.scene.mutate(element, { this.scene.mutateElement(element, {
points: [...element.points, pointFrom<LocalPoint>(0, 0)], points: [...element.points, pointFrom<LocalPoint>(0, 0)],
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
@ -8458,7 +8472,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
}; };
this.scene.mutate(croppingElement, { this.scene.mutateElement(croppingElement, {
crop: nextCrop, crop: nextCrop,
}); });
@ -8655,7 +8669,7 @@ class App extends React.Component<AppProps, AppState> {
? newElement.pressures ? newElement.pressures
: [...newElement.pressures, event.pressure]; : [...newElement.pressures, event.pressure];
this.scene.mutate( this.scene.mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
@ -8663,6 +8677,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
{ {
informMutation: false, informMutation: false,
isDragging: false,
}, },
); );
@ -8686,23 +8701,23 @@ class App extends React.Component<AppProps, AppState> {
} }
if (points.length === 1) { if (points.length === 1) {
this.scene.mutate( this.scene.mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} else if ( } else if (
points.length === 2 || points.length === 2 ||
(points.length > 1 && isElbowArrow(newElement)) (points.length > 1 && isElbowArrow(newElement))
) { ) {
this.scene.mutate( this.scene.mutateElement(
newElement, newElement,
{ {
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)], points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
}, },
{ isDragging: true }, { isDragging: true, informMutation: false },
); );
} }
@ -8916,7 +8931,7 @@ class App extends React.Component<AppProps, AppState> {
.map((e) => elementsMap.get(e.id)) .map((e) => elementsMap.get(e.id))
.filter((e) => isElbowArrow(e)) .filter((e) => isElbowArrow(e))
.forEach((e) => { .forEach((e) => {
!!e && this.scene.mutate(e, {}); !!e && this.scene.mutateElement(e, {});
}); });
} }
} }
@ -8952,7 +8967,10 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
if (element) { if (element) {
this.scene.mutate(element as ExcalidrawElbowArrowElement, {}); this.scene.mutateElement(
element as ExcalidrawElbowArrowElement,
{},
);
} }
} }
@ -9047,7 +9065,7 @@ class App extends React.Component<AppProps, AppState> {
? [] ? []
: [...newElement.pressures, childEvent.pressure]; : [...newElement.pressures, childEvent.pressure];
this.scene.mutate(newElement, { this.scene.mutateElement(newElement, {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
pressures, pressures,
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy), lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
@ -9094,7 +9112,7 @@ class App extends React.Component<AppProps, AppState> {
); );
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
this.scene.mutate( this.scene.mutateElement(
newElement, newElement,
{ {
points: [ points: [
@ -9105,7 +9123,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
], ],
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
this.setState({ this.setState({
@ -9165,7 +9183,7 @@ class App extends React.Component<AppProps, AppState> {
); );
if (newElement.width < minWidth) { if (newElement.width < minWidth) {
this.scene.mutate(newElement, { this.scene.mutateElement(newElement, {
autoResize: true, autoResize: true,
}); });
} }
@ -9215,9 +9233,14 @@ class App extends React.Component<AppProps, AppState> {
} }
if (newElement) { if (newElement) {
this.scene.mutate(newElement, getNormalizedDimensions(newElement), { this.scene.mutateElement(
newElement,
getNormalizedDimensions(newElement),
{
informMutation: false, informMutation: false,
}); isDragging: false,
},
);
// the above does not guarantee the scene to be rendered again, hence the trigger below // the above does not guarantee the scene to be rendered again, hence the trigger below
this.scene.triggerUpdate(); this.scene.triggerUpdate();
} }
@ -9249,7 +9272,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
// remove the linear element from all groups // remove the linear element from all groups
// before removing it from the frame as well // before removing it from the frame as well
this.scene.mutate(linearElement, { this.scene.mutateElement(linearElement, {
groupIds: [], groupIds: [],
}); });
@ -9278,12 +9301,12 @@ class App extends React.Component<AppProps, AppState> {
this.state.editingGroupId!, this.state.editingGroupId!,
); );
this.scene.mutate( this.scene.mutateElement(
element, element,
{ {
groupIds: element.groupIds.slice(0, index), groupIds: element.groupIds.slice(0, index),
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} }
@ -9295,12 +9318,12 @@ class App extends React.Component<AppProps, AppState> {
element.groupIds[element.groupIds.length - 1], element.groupIds[element.groupIds.length - 1],
).length < 2 ).length < 2
) { ) {
this.scene.mutate( this.scene.mutateElement(
element, element,
{ {
groupIds: [], groupIds: [],
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
); );
} }
}); });
@ -9870,12 +9893,12 @@ class App extends React.Component<AppProps, AppState> {
const dataURL = const dataURL =
this.files[fileId]?.dataURL || (await getDataURL(imageFile)); this.files[fileId]?.dataURL || (await getDataURL(imageFile));
const imageElement = this.scene.mutate( const imageElement = this.scene.mutateElement(
_imageElement, _imageElement,
{ {
fileId, fileId,
}, },
{ informMutation: false }, { informMutation: false, isDragging: false },
) as NonDeleted<InitializedExcalidrawImageElement>; ) as NonDeleted<InitializedExcalidrawImageElement>;
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>( return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
@ -9941,7 +9964,7 @@ class App extends React.Component<AppProps, AppState> {
showCursorImagePreview, showCursorImagePreview,
}); });
} catch (error: any) { } catch (error: any) {
this.scene.mutate(imageElement, { this.scene.mutateElement(imageElement, {
isDeleted: true, isDeleted: true,
}); });
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
@ -10087,7 +10110,7 @@ class App extends React.Component<AppProps, AppState> {
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
) { ) {
const placeholderSize = 100 / this.state.zoom.value; const placeholderSize = 100 / this.state.zoom.value;
this.scene.mutate(imageElement, { this.scene.mutateElement(imageElement, {
x: imageElement.x - placeholderSize / 2, x: imageElement.x - placeholderSize / 2,
y: imageElement.y - placeholderSize / 2, y: imageElement.y - placeholderSize / 2,
width: placeholderSize, width: placeholderSize,
@ -10121,7 +10144,7 @@ class App extends React.Component<AppProps, AppState> {
const x = imageElement.x + imageElement.width / 2 - width / 2; const x = imageElement.x + imageElement.width / 2 - width / 2;
const y = imageElement.y + imageElement.height / 2 - height / 2; const y = imageElement.y + imageElement.height / 2 - height / 2;
this.scene.mutate(imageElement, { this.scene.mutateElement(imageElement, {
x, x,
y, y,
width, width,
@ -10742,7 +10765,7 @@ class App extends React.Component<AppProps, AppState> {
transformHandleType, transformHandleType,
); );
this.scene.mutate( this.scene.mutateElement(
croppingElement, croppingElement,
cropElement( cropElement(
croppingElement, croppingElement,

View file

@ -71,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 &&
scene.mutate(elementToLink, { scene.mutateElement(elementToLink, {
link: nextLink, link: nextLink,
}); });
} }
@ -79,7 +79,7 @@ const ElementLinkDialog = ({
if (!nextLink && linkEdited && sourceElementId) { if (!nextLink && linkEdited && sourceElementId) {
const elementToLink = elementsMap.get(sourceElementId); const elementToLink = elementsMap.get(sourceElementId);
elementToLink && elementToLink &&
scene.mutate(elementToLink, { scene.mutateElement(elementToLink, {
link: null, link: null,
}); });
} }

View file

@ -10,7 +10,7 @@ import {
isShallowEqual, isShallowEqual,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { mutateElementWith } from "@excalidraw/element/mutateElement"; import { mutateElement } from "@excalidraw/element/mutateElement";
import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions"; import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
@ -446,7 +446,7 @@ const LayerUI = ({
if (selectedElements.length) { if (selectedElements.length) {
for (const element of selectedElements) { for (const element of selectedElements) {
mutateElementWith(element, arrayToMap(elements), { mutateElement(element, arrayToMap(elements), {
[altKey && eyeDropperState.swapPreviewOnAlt [altKey && eyeDropperState.swapPreviewOnAlt
? colorPickerType === "elementBackground" ? colorPickerType === "elementBackground"
? "strokeColor" ? "strokeColor"

View file

@ -43,14 +43,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
if (nextValue !== undefined) { if (nextValue !== undefined) {
const nextAngle = degreesToRadians(nextValue as Degrees); const nextAngle = degreesToRadians(nextValue as Degrees);
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, scene); updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
scene.mutate(boundTextElement, { angle: nextAngle }); scene.mutateElement(boundTextElement, { angle: nextAngle });
} }
return; return;
@ -69,14 +69,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, scene); updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
scene.mutate(boundTextElement, { angle: nextAngle }); scene.mutateElement(boundTextElement, { angle: nextAngle });
} }
} }
}; };

View file

@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
}; };
} }
scene.mutate(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
height: nextCropHeight, height: nextCropHeight,
}; };
scene.mutate(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),

View file

@ -68,7 +68,7 @@ const handleFontSizeChange: DragInputCallbackType<
} }
if (nextFontSize) { if (nextFontSize) {
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });
redrawTextBoundingBox( redrawTextBoundingBox(

View file

@ -53,13 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
if (!element) { if (!element) {
continue; continue;
} }
scene.mutate(element, { scene.mutateElement(element, {
angle: nextAngle, angle: nextAngle,
}); });
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) { if (boundTextElement && !isArrowElement(element)) {
scene.mutate(boundTextElement, { angle: nextAngle }); scene.mutateElement(boundTextElement, { angle: nextAngle });
} }
} }
@ -87,13 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
scene.mutate(boundTextElement, { angle: nextAngle }); scene.mutateElement(boundTextElement, { angle: nextAngle });
} }
} }
scene.triggerUpdate(); scene.triggerUpdate();

View file

@ -81,7 +81,7 @@ const resizeElementInGroup = (
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement); const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
scene.mutate(latestElement, updates); scene.mutateElement(latestElement, updates);
const boundTextElement = getBoundTextElement( const boundTextElement = getBoundTextElement(
origElement, origElement,
@ -94,7 +94,7 @@ const resizeElementInGroup = (
}); });
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) { if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
scene.mutate(latestBoundTextElement, { scene.mutateElement(latestBoundTextElement, {
fontSize: newFontSize, fontSize: newFontSize,
}); });
handleBindTextResize( handleBindTextResize(

View file

@ -84,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) {
scene.mutate(textElement, { scene.mutateElement(textElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });
@ -112,7 +112,7 @@ const handleFontSizeChange: DragInputCallbackType<
if (shouldChangeByStepSize) { if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
} }
scene.mutate(latestElement, { scene.mutateElement(latestElement, {
fontSize: nextFontSize, fontSize: nextFontSize,
}); });

View file

@ -100,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
}; };
} }
scene.mutate(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
}); });
@ -118,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),
}; };
scene.mutate(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
}); });

View file

@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
containerId: container.id, containerId: container.id,
fontSize: 20, fontSize: 20,
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(container, {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
}); });
API.setElements([container, text]); API.setElements([container, text]);

View file

@ -146,13 +146,13 @@ export const moveElement = (
-originalElement.angle as Radians, -originalElement.angle as Radians,
); );
scene.mutate( scene.mutateElement(
latestElement, latestElement,
{ {
x, x,
y, y,
}, },
{ informMutation: shouldInformMutation }, { informMutation: shouldInformMutation, isDragging: false },
); );
updateBindings(latestElement, scene); updateBindings(latestElement, scene);
@ -163,13 +163,13 @@ export const moveElement = (
if (boundTextElement) { if (boundTextElement) {
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
latestBoundTextElement && latestBoundTextElement &&
scene.mutate( scene.mutateElement(
latestBoundTextElement, latestBoundTextElement,
{ {
x: boundTextElement.x + changeInX, x: boundTextElement.x + changeInX,
y: boundTextElement.y + changeInY, y: boundTextElement.y + changeInY,
}, },
{ informMutation: shouldInformMutation }, { informMutation: shouldInformMutation, isDragging: false },
); );
} }
}; };

View file

@ -115,7 +115,7 @@ export const Hyperlink = ({
setAppState({ activeEmbeddable: null }); setAppState({ activeEmbeddable: null });
} }
if (!link) { if (!link) {
scene.mutate(element, { scene.mutateElement(element, {
link: null, link: null,
}); });
updateEmbedValidationStatus(element, false); updateEmbedValidationStatus(element, false);
@ -127,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);
scene.mutate(element, { scene.mutateElement(element, {
link, link,
}); });
updateEmbedValidationStatus(element, false); updateEmbedValidationStatus(element, false);
@ -145,7 +145,7 @@ export const Hyperlink = ({
: 1; : 1;
const hasLinkChanged = const hasLinkChanged =
embeddableLinkCache.get(element.id) !== element.link; embeddableLinkCache.get(element.id) !== element.link;
scene.mutate(element, { scene.mutateElement(element, {
...(hasLinkChanged ...(hasLinkChanged
? { ? {
width: width:
@ -170,7 +170,7 @@ export const Hyperlink = ({
} }
} }
} else { } else {
scene.mutate(element, { link }); scene.mutateElement(element, { link });
} }
}, [ }, [
element, element,
@ -231,7 +231,7 @@ export const Hyperlink = ({
const handleRemove = useCallback(() => { const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete"); trackEvent("hyperlink", "delete");
scene.mutate(element, { link: null }); scene.mutateElement(element, { link: null });
setAppState({ showHyperlinkPopup: false }); setAppState({ showHyperlinkPopup: false });
}, [setAppState, element, scene]); }, [setAppState, element, scene]);

View file

@ -259,7 +259,6 @@ export {
} from "@excalidraw/common"; } from "@excalidraw/common";
export { export {
mutateElementWith,
mutateElement, mutateElement,
newElementWith, newElementWith,
bumpVersion, bumpVersion,

View file

@ -296,7 +296,7 @@ describe("element locking", () => {
height: textSize, height: textSize,
containerId: container.id, containerId: container.id,
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
@ -337,7 +337,7 @@ describe("element locking", () => {
containerId: container.id, containerId: container.id,
locked: true, locked: true,
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
API.setElements([container, text]); API.setElements([container, text]);
@ -371,7 +371,7 @@ describe("element locking", () => {
containerId: container.id, containerId: container.id,
locked: true, locked: true,
}); });
h.app.scene.mutate(container, { h.app.scene.mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }], boundElements: [{ id: text.id, type: "text" }],
}); });
API.setElements([container, text]); API.setElements([container, text]);

View file

@ -99,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 h.app.scene.mutate<T>> ...args: Parameters<typeof h.app.scene.mutateElement<T>>
) => { ) => {
act(() => { act(() => {
h.app.scene.mutate(...args); h.app.scene.mutateElement(...args);
}); });
}; };
@ -418,7 +418,7 @@ export class API {
}); });
h.app.scene.mutate( h.app.scene.mutateElement(
rectangle, rectangle,
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
@ -452,7 +452,7 @@ export class API {
: opts?.label?.frameId ?? null, : opts?.label?.frameId ?? null,
}); });
h.app.scene.mutate( h.app.scene.mutateElement(
arrow, arrow,
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],

View file

@ -518,7 +518,7 @@ export class UI {
if (angle !== 0) { if (angle !== 0) {
act(() => { act(() => {
h.app.scene.mutate(origElement, { angle }); h.app.scene.mutateElement(origElement, { angle });
}); });
} }

View file

@ -118,7 +118,7 @@ describe("Test Linear Elements", () => {
], ],
roundness, roundness,
}); });
h.app.scene.mutate(line, { points: line.points }); h.app.scene.mutateElement(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

@ -779,6 +779,7 @@ export type UnsubscribeCallback = () => void;
export interface ExcalidrawImperativeAPI { export interface ExcalidrawImperativeAPI {
updateScene: InstanceType<typeof App>["updateScene"]; updateScene: InstanceType<typeof App>["updateScene"];
mutateElement: InstanceType<typeof App>["mutateElement"];
updateLibrary: InstanceType<typeof Library>["updateLibrary"]; updateLibrary: InstanceType<typeof Library>["updateLibrary"];
resetScene: InstanceType<typeof App>["resetScene"]; resetScene: InstanceType<typeof App>["resetScene"];
getSceneElementsIncludingDeleted: InstanceType< getSceneElementsIncludingDeleted: InstanceType<

View file

@ -199,7 +199,7 @@ export const textWysiwyg = ({
container.type, container.type,
); );
app.scene.mutate(container, { height: targetContainerHeight }); app.scene.mutateElement(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,
); );
app.scene.mutate(container, { height: targetContainerHeight }); app.scene.mutateElement(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);
} }
app.scene.mutate(updatedTextElement, { x: coordX, y: coordY }); app.scene.mutateElement(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) {
app.scene.mutate(container, { app.scene.mutateElement(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 {
app.scene.mutate(container, { app.scene.mutateElement(container, {
boundElements: container.boundElements?.filter( boundElements: container.boundElements?.filter(
(ele) => (ele) =>
!isTextElement( !isTextElement(