Deprecate mutateElement, use scene.mutateElement or mutateElementWIth instead

This commit is contained in:
Marcel Mraz 2025-04-11 22:07:42 +00:00
parent a9c3b2a4d4
commit 94c773a990
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
45 changed files with 593 additions and 704 deletions

View file

@ -679,7 +679,7 @@ export const arrayToMap = <T extends { id: string } | string>(
return items.reduce((acc: Map<string, T>, element) => { return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element); acc.set(typeof element === "string" ? element : element.id, element);
return acc; return acc;
}, new Map()); }, new Map() as Map<string, T>);
}; };
export const arrayToMapWithIndex = <T extends { id: string }>( export const arrayToMapWithIndex = <T extends { id: string }>(

View file

@ -2,11 +2,10 @@ import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds"; import { getCommonBoundingBox } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getMaximumGroups } from "./groups"; import { getMaximumGroups } from "./groups";
import type { BoundingBox } from "./bounds"; import type { BoundingBox } from "./bounds";
import type { ElementsMap, ExcalidrawElement } from "./types"; import type { ExcalidrawElement } from "./types";
export interface Alignment { export interface Alignment {
position: "start" | "center" | "end"; position: "start" | "center" | "end";
@ -15,10 +14,10 @@ export interface Alignment {
export const alignElements = ( export const alignElements = (
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment, alignment: Alignment,
scene: Scene, scene: Scene,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const elementsMap = scene.getNonDeletedElementsMap();
const groups: ExcalidrawElement[][] = getMaximumGroups( const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements, selectedElements,
elementsMap, elementsMap,
@ -33,12 +32,13 @@ export const alignElements = (
); );
return group.map((element) => { return group.map((element) => {
// update element // update element
const updatedEle = mutateElement(element, { const updatedEle = scene.mutate(element, {
x: element.x + translation.x, x: element.x + translation.x,
y: element.y + translation.y, y: element.y + translation.y,
}); });
// update bound elements // update bound elements
updateBoundElements(element, scene.getNonDeletedElementsMap(), { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: group, simultaneouslyUpdated: group,
}); });
return updatedEle; return updatedEle;

View file

@ -50,7 +50,7 @@ import {
type Heading, type Heading,
} from "./heading"; } from "./heading";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement"; import { mutateElementWith, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isArrowElement, isArrowElement,
@ -66,7 +66,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
import { mutateElbowArrow, updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement"; import type { ElementUpdate } from "./mutateElement";
@ -84,7 +84,6 @@ import type {
OrderedExcalidrawElement, OrderedExcalidrawElement,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
FixedPoint, FixedPoint,
SceneElementsMap,
FixedPointBinding, FixedPointBinding,
} from "./types"; } from "./types";
@ -130,7 +129,6 @@ export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep", startBindingElement: ExcalidrawBindableElement | null | "keep",
endBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: NonDeletedSceneElementsMap,
scene: Scene, scene: Scene,
): void => { ): void => {
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@ -142,7 +140,7 @@ export const bindOrUnbindLinearElement = (
"start", "start",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, scene,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@ -151,7 +149,7 @@ export const bindOrUnbindLinearElement = (
"end", "end",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, scene,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@ -159,7 +157,7 @@ export const bindOrUnbindLinearElement = (
); );
getNonDeletedElements(scene, onlyUnbound).forEach((element) => { getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
mutateElement(element, { scene.mutate(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,
@ -177,7 +175,7 @@ const bindOrUnbindLinearElementEdge = (
boundToElementIds: Set<ExcalidrawBindableElement["id"]>, boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
): void => { ): void => {
// "keep" is for method chaining convenience, a "no-op", so just bail out // "keep" is for method chaining convenience, a "no-op", so just bail out
if (bindableElement === "keep") { if (bindableElement === "keep") {
@ -186,7 +184,7 @@ const bindOrUnbindLinearElementEdge = (
// null means break the bind, so nothing to consider here // null means break the bind, so nothing to consider here
if (bindableElement === null) { if (bindableElement === null) {
const unbound = unbindLinearElement(linearElement, startOrEnd); const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
if (unbound != null) { if (unbound != null) {
unboundFromElementIds.add(unbound); unboundFromElementIds.add(unbound);
} }
@ -213,12 +211,19 @@ const bindOrUnbindLinearElementEdge = (
linearElement, linearElement,
bindableElement, bindableElement,
startOrEnd, startOrEnd,
elementsMap, scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
); );
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
} else { } else {
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap); bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
}; };
@ -362,11 +367,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
export const bindOrUnbindLinearElements = ( export const bindOrUnbindLinearElements = (
selectedElements: NonDeleted<ExcalidrawLinearElement>[], selectedElements: NonDeleted<ExcalidrawLinearElement>[],
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
scene: Scene,
isBindingEnabled: boolean, isBindingEnabled: boolean,
draggingPoints: readonly number[] | null, draggingPoints: readonly number[] | null,
scene: Scene,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
): void => { ): void => {
selectedElements.forEach((selectedElement) => { selectedElements.forEach((selectedElement) => {
@ -376,20 +379,20 @@ export const bindOrUnbindLinearElements = (
selectedElement, selectedElement,
isBindingEnabled, isBindingEnabled,
draggingPoints ?? [], draggingPoints ?? [],
elementsMap, scene.getNonDeletedElementsMap(),
elements, scene.getNonDeletedElements(),
zoom, zoom,
) )
: // The arrow itself (the shaft) or the inner joins are dragged : // The arrow itself (the shaft) or the inner joins are dragged
getBindingStrategyForDraggingArrowOrJoints( getBindingStrategyForDraggingArrowOrJoints(
selectedElement, selectedElement,
elementsMap, scene.getNonDeletedElementsMap(),
elements, scene.getNonDeletedElements(),
isBindingEnabled, isBindingEnabled,
zoom, zoom,
); );
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene); bindOrUnbindLinearElement(selectedElement, start, end, scene);
}); });
}; };
@ -429,22 +432,22 @@ export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState, appState: AppState,
pointerCoords: { x: number; y: number }, pointerCoords: { x: number; y: number },
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
elements: readonly NonDeletedExcalidrawElement[],
): void => { ): void => {
if (appState.startBoundElement != null) { if (appState.startBoundElement != null) {
bindLinearElement( bindLinearElement(
linearElement, linearElement,
appState.startBoundElement, appState.startBoundElement,
"start", "start",
elementsMap, scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
); );
} }
const hoveredElement = getHoveredElementForBinding( const hoveredElement = getHoveredElementForBinding(
pointerCoords, pointerCoords,
elements, scene.getNonDeletedElements(),
elementsMap, scene.getNonDeletedElementsMap(),
appState.zoom, appState.zoom,
isElbowArrow(linearElement), isElbowArrow(linearElement),
isElbowArrow(linearElement), isElbowArrow(linearElement),
@ -458,7 +461,13 @@ export const maybeBindLinearElement = (
"end", "end",
) )
) { ) {
bindLinearElement(linearElement, hoveredElement, "end", elementsMap); bindLinearElement(
linearElement,
hoveredElement,
"end",
scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
);
} }
} }
}; };
@ -487,7 +496,11 @@ export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: Map<string, ExcalidrawElement>,
mutator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => ExcalidrawElement,
): void => { ): void => {
if (!isArrowElement(linearElement)) { if (!isArrowElement(linearElement)) {
return; return;
@ -500,7 +513,7 @@ export const bindLinearElement = (
linearElement, linearElement,
hoveredElement, hoveredElement,
startOrEnd, startOrEnd,
elementsMap, elementsMap as NonDeletedSceneElementsMap,
), ),
hoveredElement, hoveredElement,
), ),
@ -513,18 +526,17 @@ export const bindLinearElement = (
linearElement, linearElement,
hoveredElement, hoveredElement,
startOrEnd, startOrEnd,
elementsMap,
), ),
}; };
} }
mutateElement(linearElement, { mutator(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)) {
mutateElement(hoveredElement, { mutator(hoveredElement, {
boundElements: (hoveredElement.boundElements || []).concat({ boundElements: (hoveredElement.boundElements || []).concat({
id: linearElement.id, id: linearElement.id,
type: "arrow", type: "arrow",
@ -566,13 +578,14 @@ const isLinearElementSimple = (
const unbindLinearElement = ( const unbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
scene: Scene,
): ExcalidrawBindableElement["id"] | null => { ): ExcalidrawBindableElement["id"] | null => {
const field = startOrEnd === "start" ? "startBinding" : "endBinding"; const field = startOrEnd === "start" ? "startBinding" : "endBinding";
const binding = linearElement[field]; const binding = linearElement[field];
if (binding == null) { if (binding == null) {
return null; return null;
} }
mutateElement(linearElement, { [field]: null }); scene.mutate(linearElement, { [field]: null });
return binding.elementId; return binding.elementId;
}; };
@ -740,7 +753,7 @@ const calculateFocusAndGap = (
// in explicitly. // in explicitly.
export const updateBoundElements = ( export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement, changedElement: NonDeletedExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: Map<string, ExcalidrawElement>,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
@ -796,20 +809,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)) {
if (isElbowArrow(element)) { mutateElementWith(element, elementsMap, bindings);
mutateElbowArrow(
element,
bindings as {
startBinding: FixedPointBinding;
endBinding: FixedPointBinding;
},
true,
elementsMap,
);
} else {
mutateElement(element, bindings, true);
}
return; return;
} }
@ -898,7 +898,6 @@ export const getHeadingForElbowArrowSnap = (
otherPoint: Readonly<GlobalPoint>, otherPoint: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement | undefined | null, bindableElement: ExcalidrawBindableElement | undefined | null,
aabb: Bounds | undefined | null, aabb: Bounds | undefined | null,
elementsMap: ElementsMap,
origPoint: GlobalPoint, origPoint: GlobalPoint,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
): Heading => { ): Heading => {
@ -908,12 +907,7 @@ export const getHeadingForElbowArrowSnap = (
return otherPointHeading; return otherPointHeading;
} }
const distance = getDistanceForBinding( const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
origPoint,
bindableElement,
elementsMap,
zoom,
);
if (!distance) { if (!distance) {
return vectorToHeading( return vectorToHeading(
@ -933,7 +927,6 @@ export const getHeadingForElbowArrowSnap = (
const getDistanceForBinding = ( const getDistanceForBinding = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement, bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
) => { ) => {
const distance = distanceToBindableElement(bindableElement, point); const distance = distanceToBindableElement(bindableElement, point);
@ -1239,7 +1232,6 @@ const updateBoundPoint = (
linearElement, linearElement,
bindableElement, bindableElement,
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>( const globalMidPoint = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
@ -1349,7 +1341,6 @@ export const calculateFixedPointForElbowArrowBinding = (
linearElement: NonDeleted<ExcalidrawElbowArrowElement>, linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): { fixedPoint: FixedPoint } => { ): { fixedPoint: FixedPoint } => {
const bounds = [ const bounds = [
hoveredElement.x, hoveredElement.x,

View file

@ -17,7 +17,6 @@ import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { getCommonBounds } from "./bounds"; import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { getMinTextElementWidth } from "./textMeasurements"; import { getMinTextElementWidth } from "./textMeasurements";
@ -104,7 +103,7 @@ export const dragSelectedElements = (
); );
elementsToUpdate.forEach((element) => { elementsToUpdate.forEach((element) => {
updateElementCoords(pointerDownState, element, adjustedOffset); updateElementCoords(pointerDownState, element, scene, adjustedOffset);
if (!isArrowElement(element)) { if (!isArrowElement(element)) {
// skip arrow labels since we calculate its position during render // skip arrow labels since we calculate its position during render
const textElement = getBoundTextElement( const textElement = getBoundTextElement(
@ -112,9 +111,14 @@ export const dragSelectedElements = (
scene.getNonDeletedElementsMap(), scene.getNonDeletedElementsMap(),
); );
if (textElement) { if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(
pointerDownState,
textElement,
scene,
adjustedOffset,
);
} }
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), { updateBoundElements(element, scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: Array.from(elementsToUpdate), simultaneouslyUpdated: Array.from(elementsToUpdate),
}); });
} }
@ -155,6 +159,7 @@ const calculateOffset = (
const updateElementCoords = ( const updateElementCoords = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
scene: Scene,
dragOffset: { x: number; y: number }, dragOffset: { x: number; y: number },
) => { ) => {
const originalElement = const originalElement =
@ -163,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;
mutateElement(element, { scene.mutate(element, {
x: nextX, x: nextX,
y: nextY, y: nextY,
}); });
@ -190,6 +195,7 @@ export const dragNewElement = ({
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
shouldResizeFromCenter, shouldResizeFromCenter,
zoom, zoom,
scene,
widthAspectRatio = null, widthAspectRatio = null,
originOffset = null, originOffset = null,
informMutation = true, informMutation = true,
@ -205,6 +211,7 @@ export const dragNewElement = ({
shouldMaintainAspectRatio: boolean; shouldMaintainAspectRatio: boolean;
shouldResizeFromCenter: boolean; shouldResizeFromCenter: boolean;
zoom: NormalizedZoomValue; zoom: NormalizedZoomValue;
scene: Scene;
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */ true */
widthAspectRatio?: number | null; widthAspectRatio?: number | null;
@ -285,7 +292,7 @@ export const dragNewElement = ({
}; };
} }
mutateElement( scene.mutate(
newElement, newElement,
{ {
x: newX + (originOffset?.x ?? 0), x: newX + (originOffset?.x ?? 0),
@ -295,7 +302,7 @@ export const dragNewElement = ({
...textAutoResize, ...textAutoResize,
...imageInitialDimension, ...imageInitialDimension,
}, },
informMutation, { informMutation },
); );
} }
}; };

View file

@ -22,8 +22,6 @@ import {
isDevEnv, isDevEnv,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
import type { AppState } from "@excalidraw/excalidraw/types"; import type { AppState } from "@excalidraw/excalidraw/types";
import { import {
@ -47,12 +45,11 @@ import {
vectorToHeading, vectorToHeading,
headingForPoint, headingForPoint,
} from "./heading"; } from "./heading";
import { mutateElement, type ElementUpdate } from "./mutateElement"; import { type ElementUpdate } from "./mutateElement";
import { isBindableElement, isElbowArrow } from "./typeChecks"; import { isBindableElement, isElbowArrow } from "./typeChecks";
import { import {
type ExcalidrawElbowArrowElement, type ExcalidrawElbowArrowElement,
type NonDeletedSceneElementsMap, type NonDeletedSceneElementsMap,
type SceneElementsMap,
} from "./types"; } from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes"; import { aabbForElement, pointInsideBounds } from "./shapes";
@ -903,38 +900,6 @@ export const elbowArrowNeedsToGetNormalized = (
); );
}; };
/**
* Mutates an elbow arrow element and renormalizes it's properties if necessary.
*/
export const mutateElbowArrow = (
element: Readonly<ExcalidrawElbowArrowElement>,
updates: ElementUpdate<ExcalidrawElbowArrowElement>,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap | ElementsMap,
options?: {
isDragging?: boolean;
},
): ElementUpdate<ExcalidrawElbowArrowElement> => {
invariant(
!isElbowArrow(element),
`Element "${element.type}" is not an elbow arrow! Use \`mutateElement\` instead`,
);
if (!elbowArrowNeedsToGetNormalized(element, updates)) {
return mutateElement(element, updates);
}
return mutateElement(element, {
...updates,
angle: 0 as Radians,
...updateElbowArrowPoints(
element,
elementsMap as NonDeletedSceneElementsMap,
updates,
options,
),
});
};
/** /**
* *
*/ */
@ -1329,14 +1294,12 @@ const getElbowArrowData = (
const startHeading = getBindPointHeading( const startHeading = getBindPointHeading(
startGlobalPoint, startGlobalPoint,
endGlobalPoint, endGlobalPoint,
elementsMap,
hoveredStartElement, hoveredStartElement,
origStartGlobalPoint, origStartGlobalPoint,
); );
const endHeading = getBindPointHeading( const endHeading = getBindPointHeading(
endGlobalPoint, endGlobalPoint,
startGlobalPoint, startGlobalPoint,
elementsMap,
hoveredEndElement, hoveredEndElement,
origEndGlobalPoint, origEndGlobalPoint,
); );
@ -2306,7 +2269,6 @@ const getGlobalPoint = (
const getBindPointHeading = ( const getBindPointHeading = (
p: GlobalPoint, p: GlobalPoint,
otherPoint: GlobalPoint, otherPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined, hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: GlobalPoint, origPoint: GlobalPoint,
): Heading => ): Heading =>
@ -2324,7 +2286,6 @@ const getBindPointHeading = (
number, number,
], ],
), ),
elementsMap,
origPoint, origPoint,
); );

View file

@ -7,6 +7,8 @@ import type {
PendingExcalidrawElements, PendingExcalidrawElements,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { bindLinearElement } from "./binding"; import { bindLinearElement } from "./binding";
import { updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import { import {
@ -239,6 +241,7 @@ const addNewNode = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
) => { ) => {
const successors = getSuccessors(element, elementsMap, direction); const successors = getSuccessors(element, elementsMap, direction);
const predeccessors = getPredecessors(element, elementsMap, direction); const predeccessors = getPredecessors(element, elementsMap, direction);
@ -277,6 +280,7 @@ const addNewNode = (
elementsMap, elementsMap,
direction, direction,
appState, appState,
scene,
); );
return { return {
@ -290,6 +294,7 @@ export const addNewNodes = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
numberOfNodes: number, numberOfNodes: number,
) => { ) => {
// always start from 0 and distribute evenly // always start from 0 and distribute evenly
@ -355,6 +360,7 @@ export const addNewNodes = (
elementsMap, elementsMap,
direction, direction,
appState, appState,
scene,
); );
newNodes.push(nextNode); newNodes.push(nextNode);
@ -370,6 +376,7 @@ const createBindingArrow = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
direction: LinkDirection, direction: LinkDirection,
appState: AppState, appState: AppState,
scene: Scene,
) => { ) => {
let startX: number; let startX: number;
let startY: number; let startY: number;
@ -444,13 +451,15 @@ const createBindingArrow = (
bindingArrow, bindingArrow,
startBindingElement, startBindingElement,
"start", "start",
elementsMap as NonDeletedSceneElementsMap, scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
); );
bindLinearElement( bindLinearElement(
bindingArrow, bindingArrow,
endBindingElement, endBindingElement,
"end", "end",
elementsMap as NonDeletedSceneElementsMap, scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
); );
const changedElements = new Map<string, OrderedExcalidrawElement>(); const changedElements = new Map<string, OrderedExcalidrawElement>();
@ -635,6 +644,7 @@ export class FlowChartCreator {
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
) { ) {
if (direction !== this.direction) { if (direction !== this.direction) {
const { nextNode, bindingArrow } = addNewNode( const { nextNode, bindingArrow } = addNewNode(
@ -642,6 +652,7 @@ export class FlowChartCreator {
elementsMap, elementsMap,
appState, appState,
direction, direction,
scene,
); );
this.numberOfNodes = 1; this.numberOfNodes = 1;
@ -655,6 +666,7 @@ export class FlowChartCreator {
elementsMap, elementsMap,
appState, appState,
direction, direction,
scene,
this.numberOfNodes, this.numberOfNodes,
); );
@ -682,13 +694,9 @@ export class FlowChartCreator {
) )
) { ) {
this.pendingNodes = this.pendingNodes.map((node) => this.pendingNodes = this.pendingNodes.map((node) =>
mutateElement( mutateElement(node, {
node, frameId: startNode.frameId,
{ }),
frameId: startNode.frameId,
},
false,
),
); );
} }
} }

View file

@ -2,7 +2,7 @@ import { generateNKeysBetween } from "fractional-indexing";
import { arrayToMap } from "@excalidraw/common"; import { arrayToMap } from "@excalidraw/common";
import { mutateElement } from "./mutateElement"; import { mutateElementWith } from "./mutateElement";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { hasBoundTextElement } from "./typeChecks"; import { hasBoundTextElement } from "./typeChecks";
@ -176,7 +176,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) {
mutateElement(element, update, false); mutateElementWith(element, arrayToMap(elements), update);
} }
} catch (e) { } catch (e) {
// fallback to default sync // fallback to default sync
@ -197,7 +197,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) {
mutateElement(element, update, false); mutateElementWith(element, arrayToMap(elements), update);
} }
return elements as OrderedExcalidrawElement[]; return elements as OrderedExcalidrawElement[];

View file

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

View file

@ -48,10 +48,8 @@ import {
getMinMaxXYFromCurvePathOps, getMinMaxXYFromCurvePathOps,
} from "./bounds"; } from "./bounds";
import { mutateElbowArrow, updateElbowArrowPoints } from "./elbowArrow";
import { headingIsHorizontal, vectorToHeading } from "./heading"; import { headingIsHorizontal, vectorToHeading } from "./heading";
import { bumpVersion, mutateElement } from "./mutateElement"; import { mutateElementWith, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { import {
isBindingElement, isBindingElement,
@ -125,15 +123,17 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean; public readonly elbowed: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>) { constructor(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
) {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
if (!pointsEqual(element.points[0], pointFrom(0, 0))) { if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack); console.error("Linear element is not normalized", Error().stack);
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element, elementsMap);
} }
this.selectedPointsIndices = null; this.selectedPointsIndices = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
this.isDragging = false; this.isDragging = false;
@ -792,11 +792,8 @@ export class LinearElementEditor {
elementsMap, elementsMap,
); );
} else if (event.altKey && appState.editingLinearElement) { } else if (event.altKey && appState.editingLinearElement) {
if ( if (linearElementEditor.lastUncommittedPoint == null) {
linearElementEditor.lastUncommittedPoint == null && scene.mutate(element, {
!isElbowArrow(element)
) {
mutateElement(element, {
points: [ points: [
...element.points, ...element.points,
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
@ -862,7 +859,6 @@ export class LinearElementEditor {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
scene, scene,
); );
} }
@ -1161,23 +1157,27 @@ export class LinearElementEditor {
y: element.y + offsetY, y: element.y + offsetY,
}; };
} }
// element-mutating methods // element-mutating methods
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static normalizePoints(
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) { element: NonDeleted<ExcalidrawLinearElement>,
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); elementsMap: ElementsMap,
) {
// TODO_SCENE: we don't need to inform mutation here?
mutateElementWith(
element,
elementsMap,
LinearElementEditor.getNormalizedPoints(element),
);
} }
static duplicateSelectedPoints( static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
appState: AppState,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): AppState {
invariant( invariant(
appState.editingLinearElement, appState.editingLinearElement,
"Not currently editing a linear element", "Not currently editing a linear element",
); );
const elementsMap = scene.getNonDeletedElementsMap();
const { selectedPointsIndices, elementId } = appState.editingLinearElement; const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
@ -1220,12 +1220,7 @@ export class LinearElementEditor {
return acc; return acc;
}, []); }, []);
const updates = { points: nextPoints }; scene.mutate(element, { points: nextPoints });
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
// 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
@ -1400,8 +1395,9 @@ export class LinearElementEditor {
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
app: AppClassProperties, app: AppClassProperties,
snapToGrid: boolean, snapToGrid: boolean,
elementsMap: ElementsMap, scene: Scene,
) { ) {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap, elementsMap,
@ -1431,12 +1427,7 @@ export class LinearElementEditor {
...element.points.slice(segmentMidpoint.index!), ...element.points.slice(segmentMidpoint.index!),
]; ];
const updates = { points }; scene.mutate(element, { points });
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
ret.pointerDownState = { ret.pointerDownState = {
...linearElementEditor.pointerDownState, ...linearElementEditor.pointerDownState,
@ -1488,28 +1479,10 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints); updates.points = Array.from(nextPoints);
if (!options?.sceneElementsMap) { // TODO_SCENE: fix
mutateElbowArrow(element, updates, true, options?.sceneElementsMap!, { mutateElementWith(element, options?.sceneElementsMap!, updates, {
isDragging: options?.isDragging, isDragging: options?.isDragging,
}); });
} else {
// The element is not in the scene, so we need to use the provided
// scene map.
Object.assign(element, {
...updates,
angle: 0 as Radians,
...updateElbowArrowPoints(
element,
options.sceneElementsMap,
updates,
{
isDragging: options?.isDragging,
},
),
});
}
bumpVersion(element);
} else { } else {
const nextCoords = getElementPointsCoords(element, nextPoints); const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points); const prevCoords = getElementPointsCoords(element, element.points);
@ -1790,8 +1763,9 @@ export class LinearElementEditor {
index: number, index: number,
x: number, x: number,
y: number, y: number,
elementsMap: ElementsMap, scene: Scene,
): LinearElementEditor { ): LinearElementEditor {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElement.elementId, linearElement.elementId,
elementsMap, elementsMap,
@ -1834,14 +1808,9 @@ 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);
mutateElbowArrow( scene.mutate(element, {
element, fixedSegments: nextFixedSegments,
{ });
fixedSegments: nextFixedSegments,
},
true,
elementsMap,
);
const point = pointFrom<GlobalPoint>( const point = pointFrom<GlobalPoint>(
element.x + element.x +
@ -1873,19 +1842,14 @@ export class LinearElementEditor {
static deleteFixedSegment( static deleteFixedSegment(
element: ExcalidrawElbowArrowElement, element: ExcalidrawElbowArrowElement,
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
index: number, index: number,
): void { ): void {
mutateElbowArrow( scene.mutate(element, {
element, fixedSegments: element.fixedSegments?.filter(
{ (segment) => segment.index !== index,
fixedSegments: element.fixedSegments?.filter( ),
(segment) => segment.index !== index, });
),
},
true,
elementsMap,
);
} }
} }

View file

@ -2,16 +2,24 @@ import {
getSizeFromPoints, getSizeFromPoints,
randomInteger, randomInteger,
getUpdatedTimestamp, getUpdatedTimestamp,
invariant,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
import type { Mutable } from "@excalidraw/common/utility-types"; import type { Mutable } from "@excalidraw/common/utility-types";
import { ShapeCache } from "./ShapeCache"; import { ShapeCache } from "./ShapeCache";
import { elbowArrowNeedsToGetNormalized } from "./elbowArrow"; import {
elbowArrowNeedsToGetNormalized,
updateElbowArrowPoints,
} from "./elbowArrow";
import type { ExcalidrawElement } from "./types"; import type {
ExcalidrawElbowArrowElement,
ExcalidrawElement,
NonDeletedSceneElementsMap,
} from "./types";
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit< export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
@ -20,8 +28,48 @@ export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
// This function tracks updates of text elements for the purposes for collaboration. // 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 version is used to compare updates when more than one user is working in
// the same drawing. Note: this will trigger the component to update. Make sure you // the same drawing. Note: this won't trigger the component to update, unlike `scene.mutate`.
// are calling it either from a React event handler or within unstable_batchedUpdates(). export const mutateElementWith = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
elementsMap: Map<string, ExcalidrawElement>,
updates: ElementUpdate<TElement>,
options?: {
isDragging?: boolean;
},
) => {
if (
elbowArrowNeedsToGetNormalized(
element,
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
)
) {
const normalizedUpdates = {
...updates,
angle: 0 as Radians,
...updateElbowArrowPoints(
element as ExcalidrawElbowArrowElement,
elementsMap as NonDeletedSceneElementsMap,
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
options,
),
} as ElementUpdate<ExcalidrawElbowArrowElement>;
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>>( export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement, element: TElement,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
@ -30,18 +78,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// casting to any because can't use `in` operator // casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fileId, fixedSegments, startBinding, endBinding } = const { points, fileId } = updates as any;
updates as any;
invariant(
elbowArrowNeedsToGetNormalized(element, {
points,
fixedSegments,
startBinding,
endBinding,
}),
"Elbow arrow should get normalized! Use `mutateElbowArrow` instead.",
);
if (typeof points !== "undefined") { if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates }; updates = { ...getSizeFromPoints(points), ...updates };

View file

@ -32,7 +32,7 @@ import {
getElementBounds, getElementBounds,
} from "./bounds"; } from "./bounds";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement"; import { mutateElementWith } from "./mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
getBoundTextElementId, getBoundTextElementId,
@ -60,8 +60,6 @@ import {
import { isInGroup } from "./groups"; import { isInGroup } from "./groups";
import { mutateElbowArrow } from "./elbowArrow";
import type { BoundingBox } from "./bounds"; import type { BoundingBox } from "./bounds";
import type { import type {
MaybeTransformHandleType, MaybeTransformHandleType,
@ -76,7 +74,6 @@ import type {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawImageElement, ExcalidrawImageElement,
ElementsMap, ElementsMap,
SceneElementsMap,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
@ -85,7 +82,6 @@ export const transformElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene, scene: Scene,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
@ -95,13 +91,13 @@ export const transformElements = (
centerX: number, centerX: number,
centerY: number, centerY: number,
): boolean => { ): boolean => {
const elementsMap = scene.getNonDeletedElementsMap();
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const [element] = selectedElements; const [element] = selectedElements;
if (transformHandleType === "rotation") { if (transformHandleType === "rotation") {
if (!isElbowArrow(element)) { if (!isElbowArrow(element)) {
rotateSingleElement( rotateSingleElement(
element, element,
elementsMap,
scene, scene,
pointerX, pointerX,
pointerY, pointerY,
@ -113,7 +109,7 @@ export const transformElements = (
resizeSingleTextElement( resizeSingleTextElement(
originalElements, originalElements,
element, element,
elementsMap, scene,
transformHandleType, transformHandleType,
shouldResizeFromCenter, shouldResizeFromCenter,
pointerX, pointerX,
@ -131,8 +127,6 @@ export const transformElements = (
getNextSingleWidthAndHeightFromPointer( getNextSingleWidthAndHeightFromPointer(
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElements,
transformHandleType, transformHandleType,
pointerX, pointerX,
pointerY, pointerY,
@ -147,8 +141,8 @@ export const transformElements = (
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElements, originalElements,
scene,
transformHandleType, transformHandleType,
{ {
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
@ -163,7 +157,6 @@ export const transformElements = (
rotateMultipleElements( rotateMultipleElements(
originalElements, originalElements,
selectedElements, selectedElements,
elementsMap,
scene, scene,
pointerX, pointerX,
pointerY, pointerY,
@ -212,13 +205,15 @@ export const transformElements = (
const rotateSingleElement = ( const rotateSingleElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
scene: Scene, scene: Scene,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1, x2, y2] = getElementAbsoluteCoords(
element,
scene.getNonDeletedElementsMap(),
);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
let angle: Radians; let angle: Radians;
@ -235,13 +230,13 @@ const rotateSingleElement = (
} }
const boundTextElementId = getBoundTextElementId(element); const boundTextElementId = getBoundTextElementId(element);
mutateElement(element, { angle }); scene.mutate(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)) {
mutateElement(textElement, { angle }); scene.mutate(textElement, { angle });
} }
} }
}; };
@ -291,12 +286,13 @@ export const measureFontSizeFromWidth = (
const resizeSingleTextElement = ( const resizeSingleTextElement = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
element: NonDeleted<ExcalidrawTextElement>, element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap, scene: Scene,
transformHandleType: TransformHandleDirection, transformHandleType: TransformHandleDirection,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element, element,
elementsMap, elementsMap,
@ -395,7 +391,7 @@ const resizeSingleTextElement = (
); );
const [nextX, nextY] = newTopLeft; const [nextX, nextY] = newTopLeft;
mutateElement(element, { scene.mutate(element, {
fontSize: metrics.size, fontSize: metrics.size,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
@ -510,14 +506,13 @@ const resizeSingleTextElement = (
autoResize: false, autoResize: false,
}; };
mutateElement(element, resizedElement); scene.mutate(element, resizedElement);
} }
}; };
const rotateMultipleElements = ( const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene, scene: Scene,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
@ -525,6 +520,7 @@ const rotateMultipleElements = (
centerX: number, centerX: number,
centerY: number, centerY: number,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
let centerAngle = let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX); (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
if (shouldRotateWithDiscreteAngle) { if (shouldRotateWithDiscreteAngle) {
@ -545,27 +541,18 @@ const rotateMultipleElements = (
(centerAngle + origAngle - element.angle) as Radians, (centerAngle + origAngle - element.angle) as Radians,
); );
if (isElbowArrow(element)) { const updates = isElbowArrow(element)
// Needed to re-route the arrow ? {
mutateElbowArrow( // Needed to re-route the arrow
element,
{
points: getArrowLocalFixedPoints(element, elementsMap), points: getArrowLocalFixedPoints(element, elementsMap),
}, }
false, : {
elementsMap,
);
} else {
mutateElement(
element,
{
x: element.x + (rotatedCX - cx), x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy), y: element.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians), angle: normalizeRadians((centerAngle + origAngle) as Radians),
}, };
false,
); scene.mutate(element, updates);
}
updateBoundElements(element, elementsMap, { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements, simultaneouslyUpdated: elements,
@ -573,15 +560,11 @@ const rotateMultipleElements = (
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) { if (boundText && !isArrowElement(element)) {
mutateElement( mutateElementWith(boundText, elementsMap, {
boundText, x: boundText.x + (rotatedCX - cx),
{ y: boundText.y + (rotatedCY - cy),
x: boundText.x + (rotatedCX - cx), angle: normalizeRadians((centerAngle + origAngle) as Radians),
y: boundText.y + (rotatedCY - cy), });
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
} }
} }
} }
@ -826,8 +809,8 @@ export const resizeSingleElement = (
nextHeight: number, nextHeight: number,
latestElement: ExcalidrawElement, latestElement: ExcalidrawElement,
origElement: ExcalidrawElement, origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
handleDirection: TransformHandleDirection, handleDirection: TransformHandleDirection,
{ {
shouldInformMutation = true, shouldInformMutation = true,
@ -840,6 +823,7 @@ export const resizeSingleElement = (
} = {}, } = {},
) => { ) => {
let boundTextFont: { fontSize?: number } = {}; let boundTextFont: { fontSize?: number } = {};
const elementsMap = scene.getNonDeletedElementsMap();
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement) { if (boundTextElement) {
@ -939,7 +923,7 @@ export const resizeSingleElement = (
} }
if ("scale" in latestElement && "scale" in origElement) { if ("scale" in latestElement && "scale" in origElement) {
mutateElement(latestElement, { scene.mutate(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],
@ -974,15 +958,19 @@ export const resizeSingleElement = (
...rescaledPoints, ...rescaledPoints,
}; };
mutateElement(latestElement, updates, shouldInformMutation); scene.mutate(latestElement, updates, {
informMutation: shouldInformMutation,
});
updateBoundElements(latestElement, elementsMap as SceneElementsMap, { const elementsMap = scene.getNonDeletedElementsMap();
updateBoundElements(latestElement, elementsMap, {
// 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 },
}); });
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { scene.mutate(boundTextElement, {
fontSize: boundTextFont.fontSize, fontSize: boundTextFont.fontSize,
}); });
} }
@ -998,8 +986,6 @@ export const resizeSingleElement = (
const getNextSingleWidthAndHeightFromPointer = ( const getNextSingleWidthAndHeightFromPointer = (
latestElement: ExcalidrawElement, latestElement: ExcalidrawElement,
origElement: ExcalidrawElement, origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap,
handleDirection: TransformHandleDirection, handleDirection: TransformHandleDirection,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
@ -1534,26 +1520,22 @@ export const resizeMultipleElements = (
} of elementsAndUpdates) { } of elementsAndUpdates) {
const { width, height, angle } = update; const { width, height, angle } = update;
scene.mutateElement(element, update, false, { scene.mutate(element, update, {
// needed for the fixed binding point udpate to take effect // needed for the fixed binding point udpate to take effect
isDragging: true, isDragging: true,
}); });
updateBoundElements(element, elementsMap as SceneElementsMap, { updateBoundElements(element, scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height }, newSize: { width, height },
}); });
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) { if (boundTextElement && boundTextFontSize) {
scene.mutateElement( scene.mutate(boundTextElement, {
boundTextElement, fontSize: boundTextFontSize,
{ angle: isLinearElement(element) ? undefined : angle,
fontSize: boundTextFontSize, });
angle: isLinearElement(element) ? undefined : angle,
},
false,
);
handleBindTextResize(element, elementsMap, handleDirection, true); handleBindTextResize(element, elementsMap, handleDirection, true);
} }
} }

View file

@ -17,6 +17,7 @@ import {
updateOriginalContainerCache, updateOriginalContainerCache,
} from "./containerCache"; } from "./containerCache";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { measureText } from "./textMeasurements"; import { measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping"; import { wrapText } from "./textWrapping";
@ -26,6 +27,8 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import type { ElementUpdate } from "./mutateElement";
import type { MaybeTransformHandleType } from "./transformHandles"; import type { MaybeTransformHandleType } from "./transformHandles";
import type { import type {
ElementsMap, ElementsMap,
@ -41,7 +44,10 @@ export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null, container: ExcalidrawElement | null,
elementsMap: ElementsMap, elementsMap: ElementsMap,
informMutation = true, mutator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => ExcalidrawElement,
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
const boundTextUpdates = { const boundTextUpdates = {
@ -90,7 +96,7 @@ export const redrawTextBoundingBox = (
metrics.height, metrics.height,
container.type, container.type,
); );
mutateElement(container, { height: nextHeight }, informMutation); mutator(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
} }
if (metrics.width > maxContainerWidth) { if (metrics.width > maxContainerWidth) {
@ -98,7 +104,7 @@ export const redrawTextBoundingBox = (
metrics.width, metrics.width,
container.type, container.type,
); );
mutateElement(container, { width: nextWidth }, informMutation); mutator(container, { width: nextWidth });
} }
const updatedTextElement = { const updatedTextElement = {
...textElement, ...textElement,
@ -113,7 +119,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.y = y; boundTextUpdates.y = y;
} }
mutateElement(textElement, boundTextUpdates, informMutation); mutator(textElement, boundTextUpdates);
}; };
export const handleBindTextResize = ( export const handleBindTextResize = (

View file

@ -188,8 +188,12 @@ describe("elbow arrow routing", () => {
scene.insertElement(rectangle2); scene.insertElement(rectangle2);
scene.insertElement(arrow); scene.insertElement(arrow);
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
bindLinearElement(arrow, rectangle1, "start", elementsMap); bindLinearElement(arrow, rectangle1, "start", elementsMap, (...args) =>
bindLinearElement(arrow, rectangle2, "end", elementsMap); scene.mutate(...args),
);
bindLinearElement(arrow, rectangle2, "end", elementsMap, (...args) =>
scene.mutate(...args),
);
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);

View file

@ -333,7 +333,7 @@ describe("line element", () => {
element, element,
element, element,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
h.app.scene.getNonDeletedElementsMap(), h.app.scene,
"ne", "ne",
); );
@ -369,7 +369,7 @@ describe("line element", () => {
element, element,
element, element,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
h.app.scene.getNonDeletedElementsMap(), h.app.scene,
"se", "se",
); );
@ -424,7 +424,7 @@ describe("line element", () => {
element, element,
element, element,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
h.app.scene.getNonDeletedElementsMap(), h.app.scene,
"e", "e",
{ {
shouldResizeFromCenter: true, shouldResizeFromCenter: true,

View file

@ -50,14 +50,8 @@ const alignSelectedElements = (
alignment: Alignment, alignment: Alignment,
) => { ) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = arrayToMap(elements);
const updatedElements = alignElements( const updatedElements = alignElements(selectedElements, alignment, app.scene);
selectedElements,
elementsMap,
alignment,
app.scene,
);
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);

View file

@ -21,27 +21,22 @@ import {
import { import {
hasBoundTextElement, hasBoundTextElement,
isElbowArrow,
isTextBindableContainer, isTextBindableContainer,
isTextElement, isTextElement,
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
} from "@excalidraw/element/typeChecks"; } from "@excalidraw/element/typeChecks";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { measureText } from "@excalidraw/element/textMeasurements"; import { measureText } from "@excalidraw/element/textMeasurements";
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex"; import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
import { newElement } from "@excalidraw/element/newElement"; import { newElement } from "@excalidraw/element/newElement";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
FixedPointBinding,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { Mutable } from "@excalidraw/common/utility-types"; import type { Mutable } from "@excalidraw/common/utility-types";
@ -81,7 +76,7 @@ export const actionUnbindText = register({
boundTextElement, boundTextElement,
elementsMap, elementsMap,
); );
mutateElement(boundTextElement as ExcalidrawTextElement, { app.scene.mutate(boundTextElement as ExcalidrawTextElement, {
containerId: null, containerId: null,
width, width,
height, height,
@ -89,7 +84,7 @@ export const actionUnbindText = register({
x, x,
y, y,
}); });
mutateElement(element, { app.scene.mutate(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id, (ele) => ele.id !== boundTextElement.id,
), ),
@ -154,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;
} }
mutateElement(textElement, { app.scene.mutate(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,
}); });
mutateElement(container, { app.scene.mutate(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
type: "text", type: "text",
id: textElement.id, id: textElement.id,
@ -171,6 +166,7 @@ export const actionBindText = register({
textElement, textElement,
container, container,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
// overwritting the cache with original container height so // overwritting the cache with original container height so
// it can be restored when unbind // it can be restored when unbind
@ -301,40 +297,26 @@ export const actionWrapTextInContainer = register({
} }
if (startBinding || endBinding) { if (startBinding || endBinding) {
const updates = { startBinding, endBinding }; app.scene.mutate(ele, {
startBinding,
if (isElbowArrow(ele)) { endBinding,
mutateElbowArrow( });
ele,
updates as {
startBinding: FixedPointBinding;
endBinding: FixedPointBinding;
},
false,
app.scene.getNonDeletedElementsMap(),
);
} else {
mutateElement(ele, updates, false);
}
} }
}); });
} }
mutateElement( app.scene.mutate(textElement, {
textElement, containerId: container.id,
{ verticalAlign: VERTICAL_ALIGN.MIDDLE,
containerId: container.id, boundElements: null,
verticalAlign: VERTICAL_ALIGN.MIDDLE, textAlign: TEXT_ALIGN.CENTER,
boundElements: null, autoResize: true,
textAlign: TEXT_ALIGN.CENTER, });
autoResize: true,
},
false,
);
redrawTextBoundingBox( redrawTextBoundingBox(
textElement, textElement,
container, container,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
updatedElements = pushContainerBelowText( updatedElements = pushContainerBelowText(

View file

@ -17,8 +17,6 @@ import {
selectGroupsForSelectedElements, selectGroupsForSelectedElements,
} from "@excalidraw/element/groups"; } from "@excalidraw/element/groups";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
import type { ExcalidrawElement } from "@excalidraw/element/types"; import type { ExcalidrawElement } from "@excalidraw/element/types";
import { t } from "../i18n"; import { t } from "../i18n";
@ -93,21 +91,14 @@ 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)) {
mutateElbowArrow( app.scene.mutate(bound, {
bound, startBinding:
{ el.id === bound.startBinding?.elementId
startBinding: ? null
el.id === bound.startBinding?.elementId : bound.startBinding,
? null endBinding:
: bound.startBinding, el.id === bound.endBinding?.elementId ? null : bound.endBinding,
endBinding: });
el.id === bound.endBinding?.elementId
? null
: bound.endBinding,
},
true,
app.scene.getNonDeletedElementsMap(),
);
} }
}); });
} }

View file

@ -25,7 +25,7 @@ import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
import { duplicateElements } from "@excalidraw/element/duplicate"; import { duplicateElements } from "@excalidraw/element/duplicate";
import type { ExcalidrawElement } from "@excalidraw/element/types"; import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { DuplicateIcon } from "../components/icons"; import { DuplicateIcon } from "../components/icons";
@ -52,7 +52,7 @@ export const actionDuplicateSelection = register({
try { try {
const newAppState = LinearElementEditor.duplicateSelectedPoints( const newAppState = LinearElementEditor.duplicateSelectedPoints(
appState, appState,
app.scene.getNonDeletedElementsMap(), app.scene,
); );
return { return {
@ -91,11 +91,16 @@ export const actionDuplicateSelection = register({
} }
} }
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
return { return {
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)), elements: nextElements,
appState: { appState: {
...appState, ...appState,
...updateLinearElementEditors(duplicatedElements), ...updateLinearElementEditors(
duplicatedElements,
arrayToMap(nextElements),
),
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
@ -131,7 +136,10 @@ export const actionDuplicateSelection = register({
), ),
}); });
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => { const updateLinearElementEditors = (
clonedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
) => {
const linears = clonedElements.filter(isLinearElement); const linears = clonedElements.filter(isLinearElement);
if (linears.length === 1) { if (linears.length === 1) {
const linear = linears[0]; const linear = linears[0];
@ -142,7 +150,7 @@ const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
if (onlySingleLinearSelected) { if (onlySingleLinearSelected) {
return { return {
selectedLinearElement: new LinearElementEditor(linear), selectedLinearElement: new LinearElementEditor(linear, elementsMap),
}; };
} }
} }

View file

@ -6,11 +6,8 @@ import {
} from "@excalidraw/element/binding"; } from "@excalidraw/element/binding";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
import { import {
isBindingElement, isBindingElement,
isElbowArrow,
isLinearElement, isLinearElement,
} from "@excalidraw/element/typeChecks"; } from "@excalidraw/element/typeChecks";
@ -19,13 +16,6 @@ import { isPathALoop } from "@excalidraw/element/shapes";
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers"; import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawElement,
} from "@excalidraw/element/types";
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
import { t } from "../i18n"; import { t } from "../i18n";
import { resetCursor } from "../cursor"; import { resetCursor } from "../cursor";
import { done } from "../components/icons"; import { done } from "../components/icons";
@ -56,7 +46,6 @@ export const actionFinalize = register({
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
scene, scene,
); );
} }
@ -82,7 +71,11 @@ export const actionFinalize = register({
scene.getElement(appState.pendingImageElementId); scene.getElement(appState.pendingImageElementId);
if (pendingImageElement) { if (pendingImageElement) {
mutateElement(pendingImageElement, { isDeleted: true }, false); scene.mutate(
pendingImageElement,
{ isDeleted: true },
{ informMutation: false },
);
} }
if (window.document.activeElement instanceof HTMLElement) { if (window.document.activeElement instanceof HTMLElement) {
@ -95,16 +88,6 @@ export const actionFinalize = register({
? appState.newElement ? appState.newElement
: null; : null;
const mutate = (updates: ElementUpdate<ExcalidrawElbowArrowElement>) =>
isElbowArrow(multiPointElement as ExcalidrawElbowArrowElement)
? mutateElbowArrow(
multiPointElement as ExcalidrawElbowArrowElement,
updates,
true,
elementsMap,
)
: mutateElement(multiPointElement as ExcalidrawElement, updates);
if (multiPointElement) { if (multiPointElement) {
// pen and mouse have hover // pen and mouse have hover
if ( if (
@ -116,7 +99,7 @@ export const actionFinalize = register({
!lastCommittedPoint || !lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint points[points.length - 1] !== lastCommittedPoint
) { ) {
mutate({ scene.mutate(multiPointElement, {
points: multiPointElement.points.slice(0, -1), points: multiPointElement.points.slice(0, -1),
}); });
} }
@ -140,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];
mutate({ scene.mutate(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])
@ -160,13 +143,7 @@ export const actionFinalize = register({
-1, -1,
arrayToMap(elements), arrayToMap(elements),
); );
maybeBindLinearElement( maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
multiPointElement,
appState,
{ x, y },
elementsMap,
elements,
);
} }
} }
@ -222,7 +199,10 @@ export const actionFinalize = register({
// To select the linear element when user has finished mutipoint editing // To select the linear element when user has finished mutipoint editing
selectedLinearElement: selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement) multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement) ? new LinearElementEditor(
multiPointElement,
arrayToMap(newElements),
)
: appState.selectedLinearElement, : appState.selectedLinearElement,
pendingImageElementId: null, pendingImageElementId: null,
}, },

View file

@ -4,10 +4,7 @@ import {
isBindingEnabled, isBindingEnabled,
} from "@excalidraw/element/binding"; } from "@excalidraw/element/binding";
import { getCommonBoundingBox } from "@excalidraw/element/bounds"; import { getCommonBoundingBox } from "@excalidraw/element/bounds";
import { import { newElementWith } from "@excalidraw/element/mutateElement";
mutateElement,
newElementWith,
} from "@excalidraw/element/mutateElement";
import { deepCopyElement } from "@excalidraw/element/duplicate"; import { deepCopyElement } from "@excalidraw/element/duplicate";
import { resizeMultipleElements } from "@excalidraw/element/resizeElements"; import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
import { import {
@ -162,11 +159,9 @@ const flipElements = (
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement), selectedElements.filter(isLinearElement),
elementsMap,
app.scene.getNonDeletedElements(),
app.scene,
isBindingEnabled(appState), isBindingEnabled(appState),
[], [],
app.scene,
appState.zoom, appState.zoom,
); );
@ -194,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) =>
mutateElement(element, { app.scene.mutate(element, {
x: element.x + diffX, x: element.x + diffX,
y: element.y + diffY, y: element.y + diffY,
}), }),
); );
elbowArrows.forEach((element) => elbowArrows.forEach((element) =>
mutateElement(element, { app.scene.mutate(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 { mutateElement } from "@excalidraw/element/mutateElement"; import { mutateElementWith } 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 {
@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
}, },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
const elementsMap = app.scene.getNonDeletedElementsMap();
const [x1, y1, x2, y2] = getCommonBounds( const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
selectedElements,
app.scene.getNonDeletedElementsMap(),
);
const PADDING = 16; const PADDING = 16;
const frame = newFrameElement({ const frame = newFrameElement({
x: x1 - PADDING, x: x1 - PADDING,
@ -196,13 +194,9 @@ 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);
mutateElement( mutateElementWith(elementInGroup, elementsMap, {
elementInGroup, groupIds: elementInGroup.groupIds.slice(0, index),
{ });
groupIds: elementInGroup.groupIds.slice(0, index),
},
false,
);
} }
} }

View file

@ -2,6 +2,8 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks"; import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
import { arrayToMap } from "@excalidraw/common";
import type { ExcalidrawLinearElement } from "@excalidraw/element/types"; import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette"; import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
@ -50,7 +52,7 @@ export const actionToggleLinearEditor = register({
const editingLinearElement = const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id appState.editingLinearElement?.elementId === selectedElement.id
? null ? null
: new LinearElementEditor(selectedElement); : new LinearElementEditor(selectedElement, arrayToMap(elements));
return { return {
appState: { appState: {
...appState, ...appState,

View file

@ -34,10 +34,7 @@ import {
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { import { newElementWith } from "@excalidraw/element/mutateElement";
mutateElement,
newElementWith,
} from "@excalidraw/element/mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
@ -68,7 +65,6 @@ import type {
FontFamilyValues, FontFamilyValues,
TextAlign, TextAlign,
VerticalAlign, VerticalAlign,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
@ -138,6 +134,7 @@ import { register } from "./register";
import type { CaptureUpdateActionType } from "../store"; import type { CaptureUpdateActionType } from "../store";
import type { AppClassProperties, AppState, Primitive } from "../types"; import type { AppClassProperties, AppState, Primitive } from "../types";
import type Scene from "../scene/Scene";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@ -207,25 +204,22 @@ export const getFormValue = function <T extends Primitive>(
const offsetElementAfterFontResize = ( const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement, prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement, nextElement: ExcalidrawTextElement,
scene: Scene,
) => { ) => {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) { if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
return nextElement; return nextElement;
} }
return mutateElement( return scene.mutate(nextElement, {
nextElement, x:
{ prevElement.textAlign === "left"
x: ? prevElement.x
prevElement.textAlign === "left" : prevElement.x +
? prevElement.x (prevElement.width - nextElement.width) /
: prevElement.x + (prevElement.textAlign === "center" ? 2 : 1),
(prevElement.width - nextElement.width) / // centering vertically is non-standard, but for Excalidraw I think
(prevElement.textAlign === "center" ? 2 : 1), // it makes sense
// centering vertically is non-standard, but for Excalidraw I think y: prevElement.y + (prevElement.height - nextElement.height) / 2,
// it makes sense });
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
},
false,
);
}; };
const changeFontSize = ( const changeFontSize = (
@ -252,9 +246,14 @@ const changeFontSize = (
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
newElement = offsetElementAfterFontResize(oldElement, newElement); newElement = offsetElementAfterFontResize(
oldElement,
newElement,
app.scene,
);
return newElement; return newElement;
} }
@ -269,10 +268,7 @@ const changeFontSize = (
includeBoundTextElement: true, includeBoundTextElement: true,
}).forEach((element) => { }).forEach((element) => {
if (isTextElement(element)) { if (isTextElement(element)) {
updateBoundElements( updateBoundElements(element, updatedElementsMap);
element,
updatedElementsMap as NonDeletedSceneElementsMap,
);
} }
}); });
@ -919,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
mutateElement(container, { ...cachedContainer }, false); app.scene.mutate(container, { ...cachedContainer });
} }
if (!skipFontFaceCheck) { if (!skipFontFaceCheck) {
@ -954,7 +950,7 @@ export const actionChangeFontFamily = register({
element, element,
container, container,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
false, (...args) => app.scene.mutate(...args),
); );
} }
} else { } else {
@ -973,7 +969,7 @@ export const actionChangeFontFamily = register({
latestElement as ExcalidrawTextElement, latestElement as ExcalidrawTextElement,
latestContainer, latestContainer,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
false, (...args) => app.scene.mutate(...args),
); );
} }
} }
@ -1180,6 +1176,7 @@ export const actionChangeTextAlign = register({
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
return newElement; return newElement;
} }
@ -1271,6 +1268,7 @@ export const actionChangeVerticalAlign = register({
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
return newElement; return newElement;
} }
@ -1670,10 +1668,17 @@ export const actionChangeArrowType = register({
newElement, newElement,
startHoveredElement, startHoveredElement,
"start", "start",
elementsMap, app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
endHoveredElement && endHoveredElement &&
bindLinearElement(newElement, endHoveredElement, "end", elementsMap); bindLinearElement(
newElement,
endHoveredElement,
"end",
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
const startBinding = const startBinding =
startElement && newElement.startBinding startElement && newElement.startBinding
@ -1684,7 +1689,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
startElement, startElement,
"start", "start",
elementsMap,
), ),
} }
: null; : null;
@ -1697,7 +1701,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
endElement, endElement,
"end", "end",
elementsMap,
), ),
} }
: null; : null;
@ -1729,7 +1732,13 @@ export const actionChangeArrowType = register({
newElement.startBinding.elementId, newElement.startBinding.elementId,
) as ExcalidrawBindableElement; ) as ExcalidrawBindableElement;
if (startElement) { if (startElement) {
bindLinearElement(newElement, startElement, "start", elementsMap); bindLinearElement(
newElement,
startElement,
"start",
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
} }
} }
if (newElement.endBinding) { if (newElement.endBinding) {
@ -1737,7 +1746,13 @@ export const actionChangeArrowType = register({
newElement.endBinding.elementId, newElement.endBinding.elementId,
) as ExcalidrawBindableElement; ) as ExcalidrawBindableElement;
if (endElement) { if (endElement) {
bindLinearElement(newElement, endElement, "end", elementsMap); bindLinearElement(
newElement,
endElement,
"end",
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
} }
} }
} }
@ -1758,6 +1773,7 @@ export const actionChangeArrowType = register({
if (selected) { if (selected) {
newState.selectedLinearElement = new LinearElementEditor( newState.selectedLinearElement = new LinearElementEditor(
selected as ExcalidrawLinearElement, selected as ExcalidrawLinearElement,
arrayToMap(elements),
); );
} }
} }

View file

@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks"; import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
import { KEYS } from "@excalidraw/common"; import { arrayToMap, KEYS } from "@excalidraw/common";
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups"; import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
@ -53,7 +53,7 @@ export const actionSelectAll = register({
// single linear element selected // single linear element selected
Object.keys(selectedElementIds).length === 1 && Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0]) isLinearElement(elements[0])
? new LinearElementEditor(elements[0]) ? new LinearElementEditor(elements[0], arrayToMap(elements))
: null, : null,
}, },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,

View file

@ -143,6 +143,7 @@ export const actionPasteStyles = register({
newElement, newElement,
container, container,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
} }

View file

@ -16,6 +16,7 @@ import {
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { import {
mutateElement, mutateElement,
mutateElementWith,
newElementWith, newElementWith,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
import { import {
@ -490,6 +491,7 @@ export class AppStateChange implements Change<AppState> {
nextElements.get( nextElements.get(
selectedLinearElementId, selectedLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>, ) as NonDeleted<ExcalidrawLinearElement>,
nextElements,
) )
: null; : null;
@ -499,6 +501,7 @@ export class AppStateChange implements Change<AppState> {
nextElements.get( nextElements.get(
editingLinearElementId, editingLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>, ) as NonDeleted<ExcalidrawLinearElement>,
nextElements,
) )
: null; : null;
@ -1498,7 +1501,12 @@ export class ElementsChange implements Change<SceneElementsMap> {
continue; continue;
} }
redrawTextBoundingBox(boundText, container, elements, false); redrawTextBoundingBox(
boundText,
container,
elements,
(element, updates) => mutateElementWith(element, elements, updates),
);
} }
} }

View file

@ -1402,7 +1402,7 @@ class App extends React.Component<AppProps, AppState> {
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => { private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
if (frame) { if (frame) {
this.scene.mutateElement(frame, { name: frame.name?.trim() || null }); this.scene.mutate(frame, { name: frame.name?.trim() || null });
} }
this.setState({ editingFrame: null }); this.setState({ editingFrame: null });
}; };
@ -1459,7 +1459,7 @@ class App extends React.Component<AppProps, AppState> {
autoFocus autoFocus
value={frameNameInEdit} value={frameNameInEdit}
onChange={(e) => { onChange={(e) => {
this.scene.mutateElement(f, { this.scene.mutate(f, {
name: e.target.value, name: e.target.value,
}); });
}} }}
@ -1950,13 +1950,21 @@ 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.mutateElement(frameElement, { this.scene.mutate(
customData: { generationData: undefined }, frameElement,
}, { informMutation: false }); {
customData: { generationData: undefined },
},
{ informMutation: false },
);
} else { } else {
this.scene.mutateElement(frameElement, { this.scene.mutate(
customData: { generationData: data }, frameElement,
}, { informMutation: false }); {
customData: { generationData: data },
},
{ informMutation: false },
);
} }
this.magicGenerations.set(frameElement.id, data); this.magicGenerations.set(frameElement.id, data);
this.triggerRender(); this.triggerRender();
@ -2127,7 +2135,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.mutateElement(child, { frameId: frame.id }); this.scene.mutate(child, { frameId: frame.id });
} }
this.setState({ this.setState({
@ -2926,8 +2934,7 @@ class App extends React.Component<AppProps, AppState> {
nonDeletedElementsMap, nonDeletedElementsMap,
), ),
), ),
this.scene.getNonDeletedElementsMap(), this.scene,
this.scene.getNonDeletedElements(),
); );
} }
@ -3329,6 +3336,7 @@ class App extends React.Component<AppProps, AppState> {
newElement, newElement,
container, container,
this.scene.getElementsMapIncludingDeleted(), this.scene.getElementsMapIncludingDeleted(),
(...args) => this.scene.mutate(...args),
); );
} }
}); });
@ -3452,7 +3460,11 @@ 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.mutateElement(initializedImageElement, { y }, { informMutation: false }); this.scene.mutate(
initializedImageElement,
{ y },
{ informMutation: false },
);
y = imageElement.y + imageElement.height + 25; y = imageElement.y + imageElement.height + 25;
@ -4177,6 +4189,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
getLinkDirectionFromKey(event.key), getLinkDirectionFromKey(event.key),
this.scene,
); );
} }
@ -4418,10 +4431,14 @@ class App extends React.Component<AppProps, AppState> {
} }
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
this.scene.mutateElement(element, { this.scene.mutate(
x: element.x + offsetX, element,
y: element.y + offsetY, {
}, { informMutation: false }); x: element.x + offsetX,
y: element.y + offsetY,
},
{ informMutation: false },
);
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
@ -4457,6 +4474,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor( editingLinearElement: new LinearElementEditor(
selectedElement, selectedElement,
this.scene.getNonDeletedElementsMap(),
), ),
}); });
} }
@ -4650,11 +4668,9 @@ class App extends React.Component<AppProps, AppState> {
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement), this.scene.getSelectedElements(this.state).filter(isLinearElement),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.scene,
this.state.zoom, this.state.zoom,
); );
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
@ -5324,7 +5340,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.mutateElement(container, { this.scene.mutate(container, {
height: newHeight, height: newHeight,
width: newWidth, width: newWidth,
}); });
@ -5378,7 +5394,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
if (!existingTextElement && shouldBindToContainer && container) { if (!existingTextElement && shouldBindToContainer && container) {
this.scene.mutateElement(container, { this.scene.mutate(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
type: "text", type: "text",
id: element.id, id: element.id,
@ -5454,7 +5470,10 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]), editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
),
}); });
return; return;
} else if ( } else if (
@ -5480,7 +5499,7 @@ class App extends React.Component<AppProps, AppState> {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment( LinearElementEditor.deleteFixedSegment(
selectedElements[0], selectedElements[0],
this.scene.getNonDeletedElementsMap(), this.scene,
midPoint, midPoint,
); );
@ -5927,7 +5946,7 @@ class App extends React.Component<AppProps, AppState> {
lastPoint, lastPoint,
) >= LINE_CONFIRM_THRESHOLD ) >= LINE_CONFIRM_THRESHOLD
) { ) {
this.scene.mutateElement( this.scene.mutate(
multiElement, multiElement,
{ {
points: [ points: [
@ -5951,7 +5970,7 @@ 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.mutateElement( this.scene.mutate(
multiElement, multiElement,
{ {
points: points.slice(0, -1), points: points.slice(0, -1),
@ -5990,7 +6009,7 @@ class App extends React.Component<AppProps, AppState> {
} }
// update last uncommitted point // update last uncommitted point
this.scene.mutateElement( this.scene.mutate(
multiElement, multiElement,
{ {
points: [ points: [
@ -6675,7 +6694,7 @@ class App extends React.Component<AppProps, AppState> {
const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
this.scene.mutateElement(pendingImageElement, { this.scene.mutate(pendingImageElement, {
x, x,
y, y,
frameId: frame ? frame.id : null, frameId: frame ? frame.id : null,
@ -7729,7 +7748,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.mutateElement(multiElement, { this.scene.mutate(multiElement, {
lastCommittedPoint: lastCommittedPoint:
multiElement.points[multiElement.points.length - 1], multiElement.points[multiElement.points.length - 1],
}); });
@ -7740,7 +7759,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.mutateElement(multiElement, { this.scene.mutate(multiElement, {
lastCommittedPoint: lastCommittedPoint:
multiElement.points[multiElement.points.length - 1], multiElement.points[multiElement.points.length - 1],
}); });
@ -7777,7 +7796,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.mutateElement(multiElement, { this.scene.mutate(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);
@ -7863,7 +7882,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
}; };
}); });
this.scene.mutateElement(element, { this.scene.mutate(element, {
points: [...element.points, pointFrom<LocalPoint>(0, 0)], points: [...element.points, pointFrom<LocalPoint>(0, 0)],
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
@ -8113,7 +8132,7 @@ class App extends React.Component<AppProps, AppState> {
index, index,
gridX, gridX,
gridY, gridY,
this.scene.getNonDeletedElementsMap(), this.scene,
); );
flushSync(() => { flushSync(() => {
@ -8218,7 +8237,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords, pointerCoords,
this, this,
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
elementsMap, this.scene,
); );
if (!ret) { if (!ret) {
return; return;
@ -8445,7 +8464,7 @@ class App extends React.Component<AppProps, AppState> {
), ),
}; };
this.scene.mutateElement(croppingElement, { this.scene.mutate(croppingElement, {
crop: nextCrop, crop: nextCrop,
}); });
@ -8642,7 +8661,7 @@ class App extends React.Component<AppProps, AppState> {
? newElement.pressures ? newElement.pressures
: [...newElement.pressures, event.pressure]; : [...newElement.pressures, event.pressure];
this.scene.mutateElement( this.scene.mutate(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
@ -8673,7 +8692,7 @@ class App extends React.Component<AppProps, AppState> {
} }
if (points.length === 1) { if (points.length === 1) {
this.scene.mutateElement( this.scene.mutate(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
@ -8684,7 +8703,7 @@ class App extends React.Component<AppProps, AppState> {
points.length === 2 || points.length === 2 ||
(points.length > 1 && isElbowArrow(newElement)) (points.length > 1 && isElbowArrow(newElement))
) { ) {
this.scene.mutateElement( this.scene.mutate(
newElement, newElement,
{ {
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)], points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
@ -8800,7 +8819,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0]) isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor(elementsWithinSelection[0]) ? new LinearElementEditor(
elementsWithinSelection[0],
this.scene.getNonDeletedElementsMap(),
)
: null, : null,
showHyperlinkPopup: showHyperlinkPopup:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
@ -8900,7 +8922,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.mutateElement(e, {}); !!e && this.scene.mutate(e, {});
}); });
} }
} }
@ -8936,10 +8958,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
if (element) { if (element) {
this.scene.mutateElement( this.scene.mutate(element as ExcalidrawElbowArrowElement, {});
element as ExcalidrawElbowArrowElement,
{},
);
} }
} }
@ -8968,7 +8987,6 @@ class App extends React.Component<AppProps, AppState> {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
this.scene, this.scene,
); );
} }
@ -9035,7 +9053,7 @@ class App extends React.Component<AppProps, AppState> {
? [] ? []
: [...newElement.pressures, childEvent.pressure]; : [...newElement.pressures, childEvent.pressure];
this.scene.mutateElement(newElement, { this.scene.mutate(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),
@ -9082,7 +9100,7 @@ class App extends React.Component<AppProps, AppState> {
); );
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
this.scene.mutateElement( this.scene.mutate(
newElement, newElement,
{ {
points: [ points: [
@ -9109,8 +9127,7 @@ class App extends React.Component<AppProps, AppState> {
newElement, newElement,
this.state, this.state,
pointerCoords, pointerCoords,
this.scene.getNonDeletedElementsMap(), this.scene,
this.scene.getNonDeletedElements(),
); );
} }
this.setState({ suggestedBindings: [], startBoundElement: null }); this.setState({ suggestedBindings: [], startBoundElement: null });
@ -9128,7 +9145,10 @@ class App extends React.Component<AppProps, AppState> {
}, },
prevState, prevState,
), ),
selectedLinearElement: new LinearElementEditor(newElement), selectedLinearElement: new LinearElementEditor(
newElement,
this.scene.getNonDeletedElementsMap(),
),
})); }));
} else { } else {
this.setState((prevState) => ({ this.setState((prevState) => ({
@ -9151,7 +9171,7 @@ class App extends React.Component<AppProps, AppState> {
); );
if (newElement.width < minWidth) { if (newElement.width < minWidth) {
this.scene.mutateElement(newElement, { this.scene.mutate(newElement, {
autoResize: true, autoResize: true,
}); });
} }
@ -9201,13 +9221,9 @@ class App extends React.Component<AppProps, AppState> {
} }
if (newElement) { if (newElement) {
this.scene.mutateElement( this.scene.mutate(newElement, getNormalizedDimensions(newElement), {
newElement, informMutation: false,
getNormalizedDimensions(newElement), });
{
informMutation: 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();
} }
@ -9239,7 +9255,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.mutateElement(linearElement, { this.scene.mutate(linearElement, {
groupIds: [], groupIds: [],
}); });
@ -9268,9 +9284,13 @@ class App extends React.Component<AppProps, AppState> {
this.state.editingGroupId!, this.state.editingGroupId!,
); );
this.scene.mutateElement(element, { this.scene.mutate(
groupIds: element.groupIds.slice(0, index), element,
}, { informMutation: false }); {
groupIds: element.groupIds.slice(0, index),
},
{ informMutation: false },
);
} }
nextElements.forEach((element) => { nextElements.forEach((element) => {
@ -9281,9 +9301,13 @@ 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.mutateElement(element, { this.scene.mutate(
groupIds: [], element,
}, { informMutation: false }); {
groupIds: [],
},
{ informMutation: false },
);
} }
}); });
@ -9392,7 +9416,10 @@ class App extends React.Component<AppProps, AppState> {
// the one we've hit // the one we've hit
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
this.setState({ this.setState({
selectedLinearElement: new LinearElementEditor(hitElement), selectedLinearElement: new LinearElementEditor(
hitElement,
this.scene.getNonDeletedElementsMap(),
),
}); });
} }
} }
@ -9517,7 +9544,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
newSelectedElements.length === 1 && newSelectedElements.length === 1 &&
isLinearElement(newSelectedElements[0]) isLinearElement(newSelectedElements[0])
? new LinearElementEditor(newSelectedElements[0]) ? new LinearElementEditor(
newSelectedElements[0],
this.scene.getNonDeletedElementsMap(),
)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
}; };
}); });
@ -9591,7 +9621,10 @@ class App extends React.Component<AppProps, AppState> {
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement) ? new LinearElementEditor(
hitElement,
this.scene.getNonDeletedElementsMap(),
)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
})); }));
} }
@ -9684,11 +9717,9 @@ class App extends React.Component<AppProps, AppState> {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
linearElements, linearElements,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.scene,
this.state.zoom, this.state.zoom,
); );
} }
@ -9845,9 +9876,13 @@ 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.mutateElement(_imageElement, { const imageElement = this.scene.mutate(
fileId, _imageElement,
}, { informMutation: false }) as NonDeleted<InitializedExcalidrawImageElement>; {
fileId,
},
{ informMutation: false },
) as NonDeleted<InitializedExcalidrawImageElement>;
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>( return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
async (resolve, reject) => { async (resolve, reject) => {
@ -9912,7 +9947,7 @@ class App extends React.Component<AppProps, AppState> {
showCursorImagePreview, showCursorImagePreview,
}); });
} catch (error: any) { } catch (error: any) {
this.scene.mutateElement(imageElement, { this.scene.mutate(imageElement, {
isDeleted: true, isDeleted: true,
}); });
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
@ -10058,7 +10093,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.mutateElement(imageElement, { this.scene.mutate(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,
@ -10092,7 +10127,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.mutateElement(imageElement, { this.scene.mutate(imageElement, {
x, x,
y, y,
width, width,
@ -10523,7 +10558,10 @@ class App extends React.Component<AppProps, AppState> {
this, this,
), ),
selectedLinearElement: isLinearElement(element) selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element) ? new LinearElementEditor(
element,
this.scene.getNonDeletedElementsMap(),
)
: null, : null,
} }
: this.state), : this.state),
@ -10556,6 +10594,7 @@ class App extends React.Component<AppProps, AppState> {
height: distance(pointerDownState.origin.y, pointerCoords.y), height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: false, shouldResizeFromCenter: false,
scene: this.scene,
zoom: this.state.zoom.value, zoom: this.state.zoom.value,
informMutation, informMutation,
}); });
@ -10621,6 +10660,7 @@ class App extends React.Component<AppProps, AppState> {
: shouldMaintainAspectRatio(event), : shouldMaintainAspectRatio(event),
shouldResizeFromCenter: shouldResizeFromCenter(event), shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value, zoom: this.state.zoom.value,
scene: this.scene,
widthAspectRatio: aspectRatio, widthAspectRatio: aspectRatio,
originOffset: this.state.originSnapOffset, originOffset: this.state.originSnapOffset,
informMutation, informMutation,
@ -10708,7 +10748,7 @@ class App extends React.Component<AppProps, AppState> {
transformHandleType, transformHandleType,
); );
this.scene.mutateElement( this.scene.mutate(
croppingElement, croppingElement,
cropElement( cropElement(
croppingElement, croppingElement,
@ -10846,7 +10886,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.originalElements, pointerDownState.originalElements,
transformHandleType, transformHandleType,
selectedElements, selectedElements,
this.scene.getElementsMapIncludingDeleted(),
this.scene, this.scene,
shouldRotateWithDiscreteAngle(event), shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),

View file

@ -5,11 +5,12 @@ import {
CLASSES, CLASSES,
DEFAULT_SIDEBAR, DEFAULT_SIDEBAR,
TOOL_TYPE, TOOL_TYPE,
arrayToMap,
capitalizeString, capitalizeString,
isShallowEqual, isShallowEqual,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { mutateElement } from "@excalidraw/element/mutateElement"; import { mutateElementWith } from "@excalidraw/element/mutateElement";
import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions"; import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import Scene from "../scene/Scene";
import { actionToggleStats } from "../actions"; import { actionToggleStats } from "../actions";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
@ -446,22 +446,18 @@ const LayerUI = ({
if (selectedElements.length) { if (selectedElements.length) {
for (const element of selectedElements) { for (const element of selectedElements) {
mutateElement( mutateElementWith(element, arrayToMap(elements), {
element, [altKey && eyeDropperState.swapPreviewOnAlt
{ ? colorPickerType === "elementBackground"
[altKey && eyeDropperState.swapPreviewOnAlt ? "strokeColor"
? colorPickerType === "elementBackground" : "backgroundColor"
? "strokeColor" : colorPickerType === "elementBackground"
: "backgroundColor" ? "backgroundColor"
: colorPickerType === "elementBackground" : "strokeColor"]: color,
? "backgroundColor" });
: "strokeColor"]: color,
},
false,
);
ShapeCache.delete(element); ShapeCache.delete(element);
} }
Scene.getScene(selectedElements[0])?.triggerUpdate(); app.scene.triggerUpdate();
} else if (colorPickerType === "elementBackground") { } else if (colorPickerType === "elementBackground") {
setAppState({ setAppState({
currentItemBackgroundColor: color, currentItemBackgroundColor: color,

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, isElbowArrow } from "@excalidraw/element/typeChecks"; import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
@ -35,7 +33,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
scene, scene,
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const origElement = originalElements[0]; const origElement = originalElements[0];
if (origElement && !isElbowArrow(origElement)) { if (origElement && !isElbowArrow(origElement)) {
const latestElement = elementsMap.get(origElement.id); const latestElement = elementsMap.get(origElement.id);
@ -45,14 +42,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
if (nextValue !== undefined) { if (nextValue !== undefined) {
const nextAngle = degreesToRadians(nextValue as Degrees); const nextAngle = degreesToRadians(nextValue as Degrees);
mutateElement(latestElement, { scene.mutate(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, elementsMap, elements, scene); updateBindings(latestElement, scene);
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 });
} }
return; return;
@ -71,14 +68,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
mutateElement(latestElement, { scene.mutate(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, elementsMap, elements, scene); updateBindings(latestElement, scene);
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 });
} }
} }
}; };

View file

@ -5,7 +5,6 @@ import {
MINIMAL_CROP_SIZE, MINIMAL_CROP_SIZE,
getUncroppedWidthAndHeight, getUncroppedWidthAndHeight,
} from "@excalidraw/element/cropElement"; } from "@excalidraw/element/cropElement";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { resizeSingleElement } from "@excalidraw/element/resizeElements"; import { resizeSingleElement } from "@excalidraw/element/resizeElements";
import { isImageElement } from "@excalidraw/element/typeChecks"; import { isImageElement } from "@excalidraw/element/typeChecks";
@ -113,7 +112,7 @@ const handleDimensionChange: DragInputCallbackType<
}; };
} }
mutateElement(element, { scene.mutate(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 +143,7 @@ const handleDimensionChange: DragInputCallbackType<
height: nextCropHeight, height: nextCropHeight,
}; };
mutateElement(element, { scene.mutate(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),
@ -176,8 +175,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldMaintainAspectRatio: keepAspectRatio, shouldMaintainAspectRatio: keepAspectRatio,
@ -223,8 +222,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldMaintainAspectRatio: keepAspectRatio, shouldMaintainAspectRatio: keepAspectRatio,

View file

@ -75,6 +75,7 @@ const handleFontSizeChange: DragInputCallbackType<
latestElement, latestElement,
scene.getContainerElement(latestElement), scene.getContainerElement(latestElement),
scene.getNonDeletedElementsMap(), scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
); );
} }
} }

View file

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

View file

@ -3,7 +3,6 @@ import { useMemo } from "react";
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common"; import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
import { updateBoundElements } from "@excalidraw/element/binding"; import { updateBoundElements } from "@excalidraw/element/binding";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { import {
rescalePointsInElement, rescalePointsInElement,
resizeSingleElement, resizeSingleElement,
@ -13,11 +12,14 @@ import {
handleBindTextResize, handleBindTextResize,
} from "@excalidraw/element/textElement"; } from "@excalidraw/element/textElement";
import { isElbowArrow, isTextElement } from "@excalidraw/element/typeChecks"; import { isTextElement } from "@excalidraw/element/typeChecks";
import { getCommonBounds } from "@excalidraw/utils"; import { getCommonBounds } from "@excalidraw/utils";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; import {
mutateElement,
mutateElementWith,
} from "@excalidraw/element/mutateElement";
import type { import type {
ElementsMap, ElementsMap,
@ -82,11 +84,7 @@ const resizeElementInGroup = (
) => { ) => {
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement); const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
if (isElbowArrow(latestElement)) { mutateElementWith(latestElement, elementsMap, updates);
mutateElbowArrow(latestElement, updates, false, elementsMap);
} else {
mutateElement(latestElement, updates, false);
}
const boundTextElement = getBoundTextElement( const boundTextElement = getBoundTextElement(
origElement, origElement,
@ -99,13 +97,9 @@ const resizeElementInGroup = (
}); });
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) { if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
mutateElement( mutateElement(latestBoundTextElement, {
latestBoundTextElement, fontSize: newFontSize,
{ });
fontSize: newFontSize,
},
false,
);
handleBindTextResize( handleBindTextResize(
latestElement, latestElement,
elementsMap, elementsMap,
@ -244,8 +238,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldInformMutation: false, shouldInformMutation: false,
@ -347,8 +341,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldInformMutation: false, shouldInformMutation: false,

View file

@ -84,19 +84,15 @@ 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( mutateElement(textElement, {
textElement, fontSize: nextFontSize,
{ });
fontSize: nextFontSize,
},
false,
);
redrawTextBoundingBox( redrawTextBoundingBox(
textElement, textElement,
scene.getContainerElement(textElement), scene.getContainerElement(textElement),
elementsMap, elementsMap,
false, (...args) => scene.mutate(...args),
); );
} }
@ -117,19 +113,15 @@ const handleFontSizeChange: DragInputCallbackType<
if (shouldChangeByStepSize) { if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
} }
mutateElement( mutateElement(latestElement, {
latestElement, fontSize: nextFontSize,
{ });
fontSize: nextFontSize,
},
false,
);
redrawTextBoundingBox( redrawTextBoundingBox(
latestElement, latestElement,
scene.getContainerElement(latestElement), scene.getContainerElement(latestElement),
elementsMap, elementsMap,
false, (...args) => scene.mutate(...args),
); );
} }

View file

@ -5,12 +5,7 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
import { getCommonBounds } from "@excalidraw/element/bounds"; import { getCommonBounds } from "@excalidraw/element/bounds";
import type { import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
ElementsMap,
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
import StatsDragInput from "./DragInput"; import StatsDragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
@ -36,13 +31,11 @@ const moveElements = (
property: MultiPositionProps["property"], property: MultiPositionProps["property"],
changeInTopX: number, changeInTopX: number,
changeInTopY: number, changeInTopY: number,
elements: readonly ExcalidrawElement[],
originalElements: readonly ExcalidrawElement[], originalElements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene, scene: Scene,
) => { ) => {
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < originalElements.length; i++) {
const origElement = originalElements[i]; const origElement = originalElements[i];
const [cx, cy] = [ const [cx, cy] = [
@ -65,8 +58,6 @@ const moveElements = (
newTopLeftX, newTopLeftX,
newTopLeftY, newTopLeftY,
origElement, origElement,
elementsMap,
elements,
scene, scene,
originalElementsMap, originalElementsMap,
false, false,
@ -78,11 +69,10 @@ const moveGroupTo = (
nextX: number, nextX: number,
nextY: number, nextY: number,
originalElements: ExcalidrawElement[], originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene, scene: Scene,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
const [x1, y1, ,] = getCommonBounds(originalElements); const [x1, y1, ,] = getCommonBounds(originalElements);
const offsetX = nextX - x1; const offsetX = nextX - x1;
const offsetY = nextY - y1; const offsetY = nextY - y1;
@ -112,8 +102,6 @@ const moveGroupTo = (
topLeftX + offsetX, topLeftX + offsetX,
topLeftY + offsetY, topLeftY + offsetY,
origElement, origElement,
elementsMap,
elements,
scene, scene,
originalElementsMap, originalElementsMap,
false, false,
@ -135,7 +123,6 @@ const handlePositionChange: DragInputCallbackType<
originalAppState, originalAppState,
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
if (nextValue !== undefined) { if (nextValue !== undefined) {
for (const atomicUnit of getAtomicUnits( for (const atomicUnit of getAtomicUnits(
@ -159,8 +146,6 @@ const handlePositionChange: DragInputCallbackType<
newTopLeftX, newTopLeftX,
newTopLeftY, newTopLeftY,
elementsInUnit.map((el) => el.original), elementsInUnit.map((el) => el.original),
elementsMap,
elements,
originalElementsMap, originalElementsMap,
scene, scene,
); );
@ -188,8 +173,6 @@ const handlePositionChange: DragInputCallbackType<
newTopLeftX, newTopLeftX,
newTopLeftY, newTopLeftY,
origElement, origElement,
elementsMap,
elements,
scene, scene,
originalElementsMap, originalElementsMap,
false, false,
@ -214,8 +197,6 @@ const handlePositionChange: DragInputCallbackType<
changeInTopX, changeInTopX,
changeInTopY, changeInTopY,
originalElements, originalElements,
originalElements,
elementsMap,
originalElementsMap, originalElementsMap,
scene, scene,
); );

View file

@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
originalAppState, originalAppState,
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const origElement = originalElements[0]; const origElement = originalElements[0];
const [cx, cy] = [ const [cx, cy] = [
origElement.x + origElement.width / 2, origElement.x + origElement.width / 2,
@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
newTopLeftX, newTopLeftX,
newTopLeftY, newTopLeftY,
origElement, origElement,
elementsMap,
elements,
scene, scene,
originalElementsMap, originalElementsMap,
); );
@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
newTopLeftX, newTopLeftX,
newTopLeftY, newTopLeftY,
origElement, origElement,
elementsMap,
elements,
scene, scene,
originalElementsMap, originalElementsMap,
); );

View file

@ -4,7 +4,6 @@ import {
bindOrUnbindLinearElements, bindOrUnbindLinearElements,
updateBoundElements, updateBoundElements,
} from "@excalidraw/element/binding"; } from "@excalidraw/element/binding";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { getBoundTextElement } from "@excalidraw/element/textElement"; import { getBoundTextElement } from "@excalidraw/element/textElement";
import { import {
isFrameLikeElement, isFrameLikeElement,
@ -24,7 +23,6 @@ import type {
ElementsMap, ElementsMap,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type Scene from "../../scene/Scene"; import type Scene from "../../scene/Scene";
@ -119,12 +117,11 @@ export const moveElement = (
newTopLeftX: number, newTopLeftX: number,
newTopLeftY: number, newTopLeftY: number,
originalElement: ExcalidrawElement, originalElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
scene: Scene, scene: Scene,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
shouldInformMutation = true, shouldInformMutation = true,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
const latestElement = elementsMap.get(originalElement.id); const latestElement = elementsMap.get(originalElement.id);
if (!latestElement) { if (!latestElement) {
return; return;
@ -148,15 +145,15 @@ export const moveElement = (
-originalElement.angle as Radians, -originalElement.angle as Radians,
); );
mutateElement( scene.mutate(
latestElement, latestElement,
{ {
x, x,
y, y,
}, },
shouldInformMutation, { informMutation: shouldInformMutation },
); );
updateBindings(latestElement, elementsMap, elements, scene); updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement( const boundTextElement = getBoundTextElement(
originalElement, originalElement,
@ -165,13 +162,13 @@ export const moveElement = (
if (boundTextElement) { if (boundTextElement) {
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
latestBoundTextElement && latestBoundTextElement &&
mutateElement( scene.mutate(
latestBoundTextElement, latestBoundTextElement,
{ {
x: boundTextElement.x + changeInX, x: boundTextElement.x + changeInX,
y: boundTextElement.y + changeInY, y: boundTextElement.y + changeInY,
}, },
shouldInformMutation, { informMutation: shouldInformMutation },
); );
} }
}; };
@ -199,8 +196,6 @@ export const getAtomicUnits = (
export const updateBindings = ( export const updateBindings = (
latestElement: ExcalidrawElement, latestElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
scene: Scene, scene: Scene,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
@ -209,16 +204,12 @@ export const updateBindings = (
}, },
) => { ) => {
if (isLinearElement(latestElement)) { if (isLinearElement(latestElement)) {
bindOrUnbindLinearElements( bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
[latestElement],
elementsMap,
elements,
scene,
true,
[],
options?.zoom,
);
} else { } else {
updateBoundElements(latestElement, elementsMap, options); updateBoundElements(
latestElement,
scene.getNonDeletedElementsMap(),
options,
);
} }
}; };

View file

@ -38,6 +38,10 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { mutateElementWith } from "@excalidraw/element/mutateElement";
import { getCommonBounds } from "@excalidraw/element/bounds";
import type { ElementConstructorOpts } from "@excalidraw/element/newElement"; import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
import type { import type {
@ -63,8 +67,6 @@ import type {
import type { MarkOptional } from "@excalidraw/common/utility-types"; import type { MarkOptional } from "@excalidraw/common/utility-types";
import { getCommonBounds } from "..";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
x: number; x: number;
@ -240,7 +242,12 @@ const bindTextToContainer = (
}), }),
}); });
redrawTextBoundingBox(textElement, container, elementsMap); redrawTextBoundingBox(
textElement,
container,
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
);
return [container, textElement] as const; return [container, textElement] as const;
}; };
@ -336,6 +343,7 @@ const bindLinearElementToElement = (
startBoundElement as ExcalidrawBindableElement, startBoundElement as ExcalidrawBindableElement,
"start", "start",
elementsMap, elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
); );
} }
} }
@ -411,6 +419,7 @@ const bindLinearElementToElement = (
endBoundElement as ExcalidrawBindableElement, endBoundElement as ExcalidrawBindableElement,
"end", "end",
elementsMap, elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
); );
} }
} }

View file

@ -259,13 +259,12 @@ export {
} from "@excalidraw/common"; } from "@excalidraw/common";
export { export {
mutateElementWith,
mutateElement, mutateElement,
newElementWith, newElementWith,
bumpVersion, bumpVersion,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
export { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
export { CaptureUpdateAction } from "./store"; export { CaptureUpdateAction } from "./store";
export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library"; export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";

View file

@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail {
this.app.scene.getNonDeletedElement( this.app.scene.getNonDeletedElement(
selectedIds[0], selectedIds[0],
) as NonDeleted<ExcalidrawLinearElement>, ) as NonDeleted<ExcalidrawLinearElement>,
this.app.scene.getNonDeletedElementsMap(),
) )
: null, : null,
}; };

View file

@ -8,10 +8,7 @@ import {
isTestEnv, isTestEnv,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { isNonDeletedElement } from "@excalidraw/element"; import { isNonDeletedElement } from "@excalidraw/element";
import { import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
isElbowArrow,
isFrameLikeElement,
} from "@excalidraw/element/typeChecks";
import { getElementsInGroup } from "@excalidraw/element/groups"; import { getElementsInGroup } from "@excalidraw/element/groups";
import { import {
@ -23,12 +20,10 @@ import {
import { getSelectedElements } from "@excalidraw/element/selection"; import { getSelectedElements } from "@excalidraw/element/selection";
import { import {
mutateElement, mutateElementWith,
type ElementUpdate, type ElementUpdate,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
@ -39,7 +34,6 @@ import type {
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
OrderedExcalidrawElement, OrderedExcalidrawElement,
Ordered, Ordered,
ExcalidrawElbowArrowElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { import type {
@ -424,8 +418,12 @@ class Scene {
}; };
// TODO_SCENE: should be accessed as app.scene through the API // TODO_SCENE: should be accessed as app.scene through the API
// TODO_SCENE: in theory actions (and most of the App handlers) should not needs this as they are ending with "replaceAllElements" anyway
// TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead // TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead
mutateElement<TElement extends Mutable<ExcalidrawElement>>( // TODO_SCENE: think one more time about moving the scene inside element (probably we will end up with it either way)
// 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().
mutate<TElement extends Mutable<ExcalidrawElement>>(
element: TElement, element: TElement,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
options: { options: {
@ -435,16 +433,9 @@ class Scene {
informMutation: true, informMutation: true,
}, },
) { ) {
if (isElbowArrow(element)) { const elementsMap = this.getNonDeletedElementsMap();
mutateElbowArrow(
element, mutateElementWith(element, elementsMap, updates, options);
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
this.getNonDeletedElementsMap(),
options,
);
} else {
mutateElement(element, updates);
}
if (options.informMutation) { if (options.informMutation) {
this.triggerUpdate(); this.triggerUpdate();

View file

@ -424,7 +424,6 @@ export class API {
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
}, },
false,
); );
return [rectangle, text]; return [rectangle, text];
@ -459,7 +458,6 @@ export class API {
{ {
boundElements: [{ type: "text", id: text.id }], boundElements: [{ type: "text", id: text.id }],
}, },
false,
); );
return [arrow, text]; return [arrow, text];

View file

@ -177,7 +177,7 @@ describe("Test Linear Elements", () => {
pointFrom<LocalPoint>(0.5, 0), pointFrom<LocalPoint>(0.5, 0),
pointFrom<LocalPoint>(100, 100), pointFrom<LocalPoint>(100, 100),
]); ]);
new LinearElementEditor(element); new LinearElementEditor(element, arrayToMap(h.elements));
expect(element.points).toEqual([ expect(element.points).toEqual([
pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(0, 0),
pointFrom<LocalPoint>(99.5, 100), pointFrom<LocalPoint>(99.5, 100),

View file

@ -13,8 +13,6 @@ import type {
ExcalidrawRectangleElement, ExcalidrawRectangleElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { Excalidraw } 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";
@ -85,15 +83,13 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
const elementsMap = h.app.scene.getNonDeletedElementsMap();
act(() => { act(() => {
// bind line to two rectangles // bind line to two rectangles
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
arrow.get() as NonDeleted<ExcalidrawLinearElement>, arrow.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement, rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement,
elementsMap, h.app.scene,
{} as Scene,
); );
}); });

View file

@ -45,7 +45,6 @@ import type {
import { actionSaveToActiveFile } from "../actions"; import { actionSaveToActiveFile } from "../actions";
import Scene from "../scene/Scene";
import { parseClipboard } from "../clipboard"; import { parseClipboard } from "../clipboard";
import { import {
actionDecreaseFontSize, actionDecreaseFontSize,
@ -130,8 +129,7 @@ export const textWysiwyg = ({
const updateWysiwygStyle = () => { const updateWysiwygStyle = () => {
const appState = app.state; const appState = app.state;
const updatedTextElement = const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
if (!updatedTextElement) { if (!updatedTextElement) {
return; return;
@ -544,7 +542,7 @@ export const textWysiwyg = ({
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
// wysiwyg on update // wysiwyg on update
cleanup(); cleanup();
const updateElement = Scene.getScene(element)?.getElement( const updateElement = app.scene.getElement(
element.id, element.id,
) as ExcalidrawTextElement; ) as ExcalidrawTextElement;
if (!updateElement) { if (!updateElement) {
@ -583,6 +581,7 @@ export const textWysiwyg = ({
updateElement, updateElement,
container, container,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
); );
} }