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 8b2df92012
42 changed files with 560 additions and 652 deletions

View file

@ -679,7 +679,7 @@ export const arrayToMap = <T extends { id: string } | string>(
return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element);
return acc;
}, new Map());
}, new Map() as Map<string, T>);
};
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 { getCommonBoundingBox } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getMaximumGroups } from "./groups";
import type { BoundingBox } from "./bounds";
import type { ElementsMap, ExcalidrawElement } from "./types";
import type { ExcalidrawElement } from "./types";
export interface Alignment {
position: "start" | "center" | "end";
@ -15,10 +14,10 @@ export interface Alignment {
export const alignElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment,
scene: Scene,
): ExcalidrawElement[] => {
const elementsMap = scene.getNonDeletedElementsMap();
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements,
elementsMap,
@ -33,12 +32,13 @@ export const alignElements = (
);
return group.map((element) => {
// update element
const updatedEle = mutateElement(element, {
const updatedEle = scene.mutateElement(element, {
x: element.x + translation.x,
y: element.y + translation.y,
});
// update bound elements
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: group,
});
return updatedEle;

View file

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

View file

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

View file

@ -22,8 +22,6 @@ import {
isDevEnv,
} from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
import type { AppState } from "@excalidraw/excalidraw/types";
import {
@ -47,12 +45,11 @@ import {
vectorToHeading,
headingForPoint,
} from "./heading";
import { mutateElement, type ElementUpdate } from "./mutateElement";
import { type ElementUpdate } from "./mutateElement";
import { isBindableElement, isElbowArrow } from "./typeChecks";
import {
type ExcalidrawElbowArrowElement,
type NonDeletedSceneElementsMap,
type SceneElementsMap,
} from "./types";
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(
startGlobalPoint,
endGlobalPoint,
elementsMap,
hoveredStartElement,
origStartGlobalPoint,
);
const endHeading = getBindPointHeading(
endGlobalPoint,
startGlobalPoint,
elementsMap,
hoveredEndElement,
origEndGlobalPoint,
);
@ -2306,7 +2269,6 @@ const getGlobalPoint = (
const getBindPointHeading = (
p: GlobalPoint,
otherPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: GlobalPoint,
): Heading =>
@ -2324,7 +2286,6 @@ const getBindPointHeading = (
number,
],
),
elementsMap,
origPoint,
);

View file

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

View file

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

View file

@ -57,13 +57,9 @@ export const bindElementsToFramesAfterDuplication = (
if (nextElementId) {
const nextElement = nextElementMap.get(nextElementId);
if (nextElement) {
mutateElement(
nextElement,
{
frameId: nextFrameId ?? element.frameId,
},
false,
);
mutateElement(nextElement, {
frameId: nextFrameId ?? element.frameId,
});
}
}
}
@ -567,13 +563,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
}
for (const element of finalElementsToAdd) {
mutateElement(
element,
{
frameId: frame.id,
},
false,
);
mutateElement(element, {
frameId: frame.id,
});
}
return allElements;
@ -611,13 +603,9 @@ export const removeElementsFromFrame = (
}
for (const [, element] of _elementsToRemove) {
mutateElement(
element,
{
frameId: null,
},
false,
);
mutateElement(element, {
frameId: null,
});
}
};

View file

@ -48,10 +48,8 @@ import {
getMinMaxXYFromCurvePathOps,
} from "./bounds";
import { mutateElbowArrow, updateElbowArrowPoints } from "./elbowArrow";
import { headingIsHorizontal, vectorToHeading } from "./heading";
import { bumpVersion, mutateElement } from "./mutateElement";
import { mutateElementWith, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import {
isBindingElement,
@ -125,15 +123,17 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
constructor(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
) {
this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId";
};
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack);
LinearElementEditor.normalizePoints(element);
LinearElementEditor.normalizePoints(element, elementsMap);
}
this.selectedPointsIndices = null;
this.lastUncommittedPoint = null;
this.isDragging = false;
@ -796,7 +796,7 @@ export class LinearElementEditor {
linearElementEditor.lastUncommittedPoint == null &&
!isElbowArrow(element)
) {
mutateElement(element, {
scene.mutateElement(element, {
points: [
...element.points,
LinearElementEditor.createPointAt(
@ -862,7 +862,6 @@ export class LinearElementEditor {
element,
startBindingElement,
endBindingElement,
elementsMap,
scene,
);
}
@ -1161,23 +1160,26 @@ export class LinearElementEditor {
y: element.y + offsetY,
};
}
// element-mutating methods
// ---------------------------------------------------------------------------
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
static normalizePoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
) {
mutateElementWith(
element,
elementsMap,
LinearElementEditor.getNormalizedPoints(element),
);
}
static duplicateSelectedPoints(
appState: AppState,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): AppState {
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
invariant(
appState.editingLinearElement,
"Not currently editing a linear element",
);
const elementsMap = scene.getNonDeletedElementsMap();
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap);
@ -1220,12 +1222,7 @@ export class LinearElementEditor {
return acc;
}, []);
const updates = { points: nextPoints };
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
scene.mutateElement(element, { points: nextPoints });
// temp hack to ensure the line doesn't move when adding point to the end,
// potentially expanding the bounding box
@ -1400,8 +1397,9 @@ export class LinearElementEditor {
pointerCoords: PointerCoords,
app: AppClassProperties,
snapToGrid: boolean,
elementsMap: ElementsMap,
scene: Scene,
) {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
elementsMap,
@ -1431,12 +1429,7 @@ export class LinearElementEditor {
...element.points.slice(segmentMidpoint.index!),
];
const updates = { points };
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
scene.mutateElement(element, { points });
ret.pointerDownState = {
...linearElementEditor.pointerDownState,
@ -1488,28 +1481,10 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints);
if (!options?.sceneElementsMap) {
mutateElbowArrow(element, updates, true, options?.sceneElementsMap!, {
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);
// TODO_SCENE: fix
mutateElementWith(element, options?.sceneElementsMap!, updates, {
isDragging: options?.isDragging,
});
} else {
const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points);
@ -1790,8 +1765,9 @@ export class LinearElementEditor {
index: number,
x: number,
y: number,
elementsMap: ElementsMap,
scene: Scene,
): LinearElementEditor {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(
linearElement.elementId,
elementsMap,
@ -1834,14 +1810,9 @@ export class LinearElementEditor {
.map((segment) => segment.index)
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
mutateElbowArrow(
element,
{
fixedSegments: nextFixedSegments,
},
true,
elementsMap,
);
scene.mutateElement(element, {
fixedSegments: nextFixedSegments,
});
const point = pointFrom<GlobalPoint>(
element.x +
@ -1873,19 +1844,14 @@ export class LinearElementEditor {
static deleteFixedSegment(
element: ExcalidrawElbowArrowElement,
elementsMap: NonDeletedSceneElementsMap,
scene: Scene,
index: number,
): void {
mutateElbowArrow(
element,
{
fixedSegments: element.fixedSegments?.filter(
(segment) => segment.index !== index,
),
},
true,
elementsMap,
);
scene.mutateElement(element, {
fixedSegments: element.fixedSegments?.filter(
(segment) => segment.index !== index,
),
});
}
}

View file

@ -2,16 +2,24 @@ import {
getSizeFromPoints,
randomInteger,
getUpdatedTimestamp,
invariant,
} from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
import type { Mutable } from "@excalidraw/common/utility-types";
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<
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.
// 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
// are calling it either from a React event handler or within unstable_batchedUpdates().
// the same drawing. Note: this won't trigger the component to update, unlike `scene.mutateElement`
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.mutateElement` as direct equivalent, or `mutateElementWith` in case you don't need to trigger component update.
*/
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
@ -30,18 +78,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fileId, fixedSegments, startBinding, endBinding } =
updates as any;
invariant(
elbowArrowNeedsToGetNormalized(element, {
points,
fixedSegments,
startBinding,
endBinding,
}),
"Elbow arrow should get normalized! Use `mutateElbowArrow` instead.",
);
const { points, fileId } = updates as any;
if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates };

View file

@ -32,7 +32,7 @@ import {
getElementBounds,
} from "./bounds";
import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { mutateElementWith, mutateElement } from "./mutateElement";
import {
getBoundTextElement,
getBoundTextElementId,
@ -60,8 +60,6 @@ import {
import { isInGroup } from "./groups";
import { mutateElbowArrow } from "./elbowArrow";
import type { BoundingBox } from "./bounds";
import type {
MaybeTransformHandleType,
@ -76,7 +74,6 @@ import type {
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
ElementsMap,
SceneElementsMap,
ExcalidrawElbowArrowElement,
} from "./types";
@ -85,7 +82,6 @@ export const transformElements = (
originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene,
shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean,
@ -95,13 +91,13 @@ export const transformElements = (
centerX: number,
centerY: number,
): boolean => {
const elementsMap = scene.getNonDeletedElementsMap();
if (selectedElements.length === 1) {
const [element] = selectedElements;
if (transformHandleType === "rotation") {
if (!isElbowArrow(element)) {
rotateSingleElement(
element,
elementsMap,
scene,
pointerX,
pointerY,
@ -113,7 +109,7 @@ export const transformElements = (
resizeSingleTextElement(
originalElements,
element,
elementsMap,
scene.getNonDeletedElementsMap(),
transformHandleType,
shouldResizeFromCenter,
pointerX,
@ -123,7 +119,7 @@ export const transformElements = (
return true;
} else if (transformHandleType) {
const elementId = selectedElements[0].id;
const latestElement = elementsMap.get(elementId);
const latestElement = scene.getNonDeletedElementsMap().get(elementId);
const origElement = originalElements.get(elementId);
if (latestElement && origElement) {
@ -147,8 +143,8 @@ export const transformElements = (
nextHeight,
latestElement,
origElement,
elementsMap,
originalElements,
scene,
transformHandleType,
{
shouldMaintainAspectRatio,
@ -163,7 +159,6 @@ export const transformElements = (
rotateMultipleElements(
originalElements,
selectedElements,
elementsMap,
scene,
pointerX,
pointerY,
@ -212,13 +207,15 @@ export const transformElements = (
const rotateSingleElement = (
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
scene: Scene,
pointerX: number,
pointerY: number,
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 cy = (y1 + y2) / 2;
let angle: Radians;
@ -235,13 +232,13 @@ const rotateSingleElement = (
}
const boundTextElementId = getBoundTextElementId(element);
mutateElement(element, { angle });
scene.mutateElement(element, { angle });
if (boundTextElementId) {
const textElement =
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
if (textElement && !isArrowElement(element)) {
mutateElement(textElement, { angle });
scene.mutateElement(textElement, { angle });
}
}
};
@ -517,7 +514,6 @@ const resizeSingleTextElement = (
const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene,
pointerX: number,
pointerY: number,
@ -525,6 +521,7 @@ const rotateMultipleElements = (
centerX: number,
centerY: number,
) => {
const elementsMap = scene.getNonDeletedElementsMap();
let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
if (shouldRotateWithDiscreteAngle) {
@ -547,24 +544,15 @@ const rotateMultipleElements = (
if (isElbowArrow(element)) {
// Needed to re-route the arrow
mutateElbowArrow(
element,
{
points: getArrowLocalFixedPoints(element, elementsMap),
},
false,
elementsMap,
);
mutateElementWith(element, elementsMap, {
points: getArrowLocalFixedPoints(element, elementsMap),
});
} else {
mutateElement(
element,
{
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
mutateElement(element, {
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
});
}
updateBoundElements(element, elementsMap, {
@ -573,15 +561,11 @@ const rotateMultipleElements = (
const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) {
mutateElement(
boundText,
{
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
mutateElement(boundText, {
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
});
}
}
}
@ -826,8 +810,8 @@ export const resizeSingleElement = (
nextHeight: number,
latestElement: ExcalidrawElement,
origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
handleDirection: TransformHandleDirection,
{
shouldInformMutation = true,
@ -840,7 +824,10 @@ export const resizeSingleElement = (
} = {},
) => {
let boundTextFont: { fontSize?: number } = {};
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
const boundTextElement = getBoundTextElement(
latestElement,
scene.getNonDeletedElementsMap(),
);
if (boundTextElement) {
const stateOfBoundTextElementAtResize = originalElementsMap.get(
@ -860,7 +847,7 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
elementsMap,
scene.getNonDeletedElementsMap(),
getBoundTextMaxWidth(updatedElement, boundTextElement),
);
if (nextFont === null) {
@ -939,7 +926,7 @@ export const resizeSingleElement = (
}
if ("scale" in latestElement && "scale" in origElement) {
mutateElement(latestElement, {
scene.mutateElement(latestElement, {
scale: [
// defaulting because scaleX/Y can be 0/-0
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
@ -974,21 +961,23 @@ export const resizeSingleElement = (
...rescaledPoints,
};
mutateElement(latestElement, updates, shouldInformMutation);
scene.mutateElement(latestElement, updates, {
informMutation: shouldInformMutation,
});
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
updateBoundElements(latestElement, scene.getNonDeletedElementsMap(), {
// TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight },
});
if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, {
scene.mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
});
}
handleBindTextResize(
latestElement,
elementsMap,
scene.getNonDeletedElementsMap(),
handleDirection,
shouldMaintainAspectRatio,
);
@ -1534,26 +1523,22 @@ export const resizeMultipleElements = (
} of elementsAndUpdates) {
const { width, height, angle } = update;
scene.mutateElement(element, update, false, {
scene.mutateElement(element, update, {
// needed for the fixed binding point udpate to take effect
isDragging: true,
});
updateBoundElements(element, elementsMap as SceneElementsMap, {
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height },
});
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) {
scene.mutateElement(
boundTextElement,
{
fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle,
},
false,
);
scene.mutateElement(boundTextElement, {
fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle,
});
handleBindTextResize(element, elementsMap, handleDirection, true);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,8 +17,6 @@ import {
selectGroupsForSelectedElements,
} from "@excalidraw/element/groups";
import { mutateElbowArrow } from "@excalidraw/element/elbowArrow";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { t } from "../i18n";
@ -93,21 +91,14 @@ const deleteSelectedElements = (
el.boundElements.forEach((candidate) => {
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
if (bound && isElbowArrow(bound)) {
mutateElbowArrow(
bound,
{
startBinding:
el.id === bound.startBinding?.elementId
? null
: bound.startBinding,
endBinding:
el.id === bound.endBinding?.elementId
? null
: bound.endBinding,
},
true,
app.scene.getNonDeletedElementsMap(),
);
app.scene.mutateElement(bound, {
startBinding:
el.id === bound.startBinding?.elementId
? null
: bound.startBinding,
endBinding:
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
});
}
});
}

View file

@ -25,7 +25,7 @@ import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
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 { DuplicateIcon } from "../components/icons";
@ -52,7 +52,7 @@ export const actionDuplicateSelection = register({
try {
const newAppState = LinearElementEditor.duplicateSelectedPoints(
appState,
app.scene.getNonDeletedElementsMap(),
app.scene,
);
return {
@ -91,11 +91,16 @@ export const actionDuplicateSelection = register({
}
}
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
return {
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
elements: nextElements,
appState: {
...appState,
...updateLinearElementEditors(duplicatedElements),
...updateLinearElementEditors(
duplicatedElements,
arrayToMap(nextElements),
),
...selectGroupsForSelectedElements(
{
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);
if (linears.length === 1) {
const linear = linears[0];
@ -142,7 +150,7 @@ const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
if (onlySingleLinearSelected) {
return {
selectedLinearElement: new LinearElementEditor(linear),
selectedLinearElement: new LinearElementEditor(linear, elementsMap),
};
}
}

View file

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

View file

@ -4,10 +4,7 @@ import {
isBindingEnabled,
} from "@excalidraw/element/binding";
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
import {
mutateElement,
newElementWith,
} from "@excalidraw/element/mutateElement";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { deepCopyElement } from "@excalidraw/element/duplicate";
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
import {
@ -162,11 +159,9 @@ const flipElements = (
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
elementsMap,
app.scene.getNonDeletedElements(),
app.scene,
isBindingEnabled(appState),
[],
app.scene,
appState.zoom,
);
@ -194,13 +189,13 @@ const flipElements = (
getCommonBoundingBox(selectedElements);
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
otherElements.forEach((element) =>
mutateElement(element, {
app.scene.mutateElement(element, {
x: element.x + diffX,
y: element.y + diffY,
}),
);
elbowArrows.forEach((element) =>
mutateElement(element, {
app.scene.mutateElement(element, {
x: element.x + diffX,
y: element.y + diffY,
}),

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ import {
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
mutateElement,
mutateElementWith,
newElementWith,
} from "@excalidraw/element/mutateElement";
import {
@ -490,6 +491,7 @@ export class AppStateChange implements Change<AppState> {
nextElements.get(
selectedLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>,
nextElements,
)
: null;
@ -499,6 +501,7 @@ export class AppStateChange implements Change<AppState> {
nextElements.get(
editingLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>,
nextElements,
)
: null;
@ -1316,7 +1319,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
nextElements: SceneElementsMap,
) {
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
const updater = (
const updator = (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => {
@ -1347,12 +1350,12 @@ export class ElementsChange implements Change<SceneElementsMap> {
// removed delta is affecting the bindings always, as all the affected elements of the removed elements need to be unbound
for (const [id] of this.removed) {
ElementsChange.unbindAffected(prevElements, nextElements, id, updater);
ElementsChange.unbindAffected(prevElements, nextElements, id, updator);
}
// added delta is affecting the bindings always, all the affected elements of the added elements need to be rebound
for (const [id] of this.added) {
ElementsChange.rebindAffected(prevElements, nextElements, id, updater);
ElementsChange.rebindAffected(prevElements, nextElements, id, updator);
}
// updated delta is affecting the binding only in case it contains changed binding or bindable property
@ -1367,7 +1370,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
continue;
}
ElementsChange.rebindAffected(prevElements, nextElements, id, updater);
ElementsChange.rebindAffected(prevElements, nextElements, id, updator);
}
// filter only previous elements, which were now affected
@ -1429,7 +1432,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
prevElements: SceneElementsMap,
nextElements: SceneElementsMap,
id: string,
updater: (
updator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => void,
@ -1438,8 +1441,8 @@ export class ElementsChange implements Change<SceneElementsMap> {
const prevElement = () => prevElements.get(id); // element before addition / update
const nextElement = () => nextElements.get(id); // element after addition / update
BoundElement.unbindAffected(nextElements, prevElement(), updater);
BoundElement.rebindAffected(nextElements, nextElement(), updater);
BoundElement.unbindAffected(nextElements, prevElement(), updator);
BoundElement.rebindAffected(nextElements, nextElement(), updator);
BindableElement.unbindAffected(
nextElements,
@ -1448,11 +1451,11 @@ export class ElementsChange implements Change<SceneElementsMap> {
// we cannot rebind arrows with bindable element so we don't unbind them at all during rebind (we still need to unbind them on removal)
// TODO: #7348 add startBinding / endBinding to the `BoundElement` context so that we could rebind arrows and remove this condition
if (isTextElement(element)) {
updater(element, updates);
updator(element, updates);
}
},
);
BindableElement.rebindAffected(nextElements, nextElement(), updater);
BindableElement.rebindAffected(nextElements, nextElement(), updator);
}
private static redrawTextBoundingBoxes(
@ -1498,7 +1501,12 @@ export class ElementsChange implements Change<SceneElementsMap> {
continue;
}
redrawTextBoundingBox(boundText, container, elements, false);
redrawTextBoundingBox(
boundText,
container,
elements,
(element, updates) => mutateElementWith(element, elements, updates),
);
}
}

View file

@ -1950,13 +1950,21 @@ class App extends React.Component<AppProps, AppState> {
// state only.
// Thus reset so that we prefer local cache (if there was some
// generationData set previously)
this.scene.mutateElement(frameElement, {
customData: { generationData: undefined },
}, { informMutation: false });
this.scene.mutateElement(
frameElement,
{
customData: { generationData: undefined },
},
{ informMutation: false },
);
} else {
this.scene.mutateElement(frameElement, {
customData: { generationData: data },
}, { informMutation: false });
this.scene.mutateElement(
frameElement,
{
customData: { generationData: data },
},
{ informMutation: false },
);
}
this.magicGenerations.set(frameElement.id, data);
this.triggerRender();
@ -2926,8 +2934,7 @@ class App extends React.Component<AppProps, AppState> {
nonDeletedElementsMap,
),
),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
);
}
@ -3329,6 +3336,7 @@ class App extends React.Component<AppProps, AppState> {
newElement,
container,
this.scene.getElementsMapIncludingDeleted(),
(...args) => this.scene.mutateElement(...args),
);
}
});
@ -3452,7 +3460,11 @@ class App extends React.Component<AppProps, AppState> {
}
// hack to reset the `y` coord because we vertically center during
// insertImageElement
this.scene.mutateElement(initializedImageElement, { y }, { informMutation: false });
this.scene.mutateElement(
initializedImageElement,
{ y },
{ informMutation: false },
);
y = imageElement.y + imageElement.height + 25;
@ -4177,6 +4189,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
this.state,
getLinkDirectionFromKey(event.key),
this.scene,
);
}
@ -4418,10 +4431,14 @@ class App extends React.Component<AppProps, AppState> {
}
selectedElements.forEach((element) => {
this.scene.mutateElement(element, {
x: element.x + offsetX,
y: element.y + offsetY,
}, { informMutation: false });
this.scene.mutateElement(
element,
{
x: element.x + offsetX,
y: element.y + offsetY,
},
{ informMutation: false },
);
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements,
@ -4457,6 +4474,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({
editingLinearElement: new LinearElementEditor(
selectedElement,
this.scene.getNonDeletedElementsMap(),
),
});
}
@ -4650,11 +4668,9 @@ class App extends React.Component<AppProps, AppState> {
if (isArrowKey(event.key)) {
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.scene,
this.state.zoom,
);
this.setState({ suggestedBindings: [] });
@ -5454,7 +5470,10 @@ class App extends React.Component<AppProps, AppState> {
) {
this.store.shouldCaptureIncrement();
this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]),
editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
),
});
return;
} else if (
@ -5480,7 +5499,7 @@ class App extends React.Component<AppProps, AppState> {
this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
this.scene,
midPoint,
);
@ -8113,7 +8132,7 @@ class App extends React.Component<AppProps, AppState> {
index,
gridX,
gridY,
this.scene.getNonDeletedElementsMap(),
this.scene,
);
flushSync(() => {
@ -8218,7 +8237,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords,
this,
!event[KEYS.CTRL_OR_CMD],
elementsMap,
this.scene,
);
if (!ret) {
return;
@ -8800,7 +8819,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement:
elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor(elementsWithinSelection[0])
? new LinearElementEditor(
elementsWithinSelection[0],
this.scene.getNonDeletedElementsMap(),
)
: null,
showHyperlinkPopup:
elementsWithinSelection.length === 1 &&
@ -8968,7 +8990,6 @@ class App extends React.Component<AppProps, AppState> {
element,
startBindingElement,
endBindingElement,
elementsMap,
this.scene,
);
}
@ -9109,8 +9130,7 @@ class App extends React.Component<AppProps, AppState> {
newElement,
this.state,
pointerCoords,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
);
}
this.setState({ suggestedBindings: [], startBoundElement: null });
@ -9128,7 +9148,10 @@ class App extends React.Component<AppProps, AppState> {
},
prevState,
),
selectedLinearElement: new LinearElementEditor(newElement),
selectedLinearElement: new LinearElementEditor(
newElement,
this.scene.getNonDeletedElementsMap(),
),
}));
} else {
this.setState((prevState) => ({
@ -9268,9 +9291,13 @@ class App extends React.Component<AppProps, AppState> {
this.state.editingGroupId!,
);
this.scene.mutateElement(element, {
groupIds: element.groupIds.slice(0, index),
}, { informMutation: false });
this.scene.mutateElement(
element,
{
groupIds: element.groupIds.slice(0, index),
},
{ informMutation: false },
);
}
nextElements.forEach((element) => {
@ -9281,9 +9308,13 @@ class App extends React.Component<AppProps, AppState> {
element.groupIds[element.groupIds.length - 1],
).length < 2
) {
this.scene.mutateElement(element, {
groupIds: [],
}, { informMutation: false });
this.scene.mutateElement(
element,
{
groupIds: [],
},
{ informMutation: false },
);
}
});
@ -9392,7 +9423,10 @@ class App extends React.Component<AppProps, AppState> {
// the one we've hit
if (selectedElements.length === 1) {
this.setState({
selectedLinearElement: new LinearElementEditor(hitElement),
selectedLinearElement: new LinearElementEditor(
hitElement,
this.scene.getNonDeletedElementsMap(),
),
});
}
}
@ -9517,7 +9551,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement:
newSelectedElements.length === 1 &&
isLinearElement(newSelectedElements[0])
? new LinearElementEditor(newSelectedElements[0])
? new LinearElementEditor(
newSelectedElements[0],
this.scene.getNonDeletedElementsMap(),
)
: prevState.selectedLinearElement,
};
});
@ -9591,7 +9628,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.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement)
? new LinearElementEditor(
hitElement,
this.scene.getNonDeletedElementsMap(),
)
: prevState.selectedLinearElement,
}));
}
@ -9684,11 +9724,9 @@ class App extends React.Component<AppProps, AppState> {
bindOrUnbindLinearElements(
linearElements,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.scene,
this.state.zoom,
);
}
@ -9845,9 +9883,13 @@ class App extends React.Component<AppProps, AppState> {
const dataURL =
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
const imageElement = this.scene.mutateElement(_imageElement, {
fileId,
}, { informMutation: false }) as NonDeleted<InitializedExcalidrawImageElement>;
const imageElement = this.scene.mutateElement(
_imageElement,
{
fileId,
},
{ informMutation: false },
) as NonDeleted<InitializedExcalidrawImageElement>;
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
async (resolve, reject) => {
@ -10523,7 +10565,10 @@ class App extends React.Component<AppProps, AppState> {
this,
),
selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element)
? new LinearElementEditor(
element,
this.scene.getNonDeletedElementsMap(),
)
: null,
}
: this.state),
@ -10556,6 +10601,7 @@ class App extends React.Component<AppProps, AppState> {
height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: false,
scene: this.scene,
zoom: this.state.zoom.value,
informMutation,
});
@ -10621,6 +10667,7 @@ class App extends React.Component<AppProps, AppState> {
: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value,
scene: this.scene,
widthAspectRatio: aspectRatio,
originOffset: this.state.originSnapOffset,
informMutation,
@ -10846,7 +10893,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.originalElements,
transformHandleType,
selectedElements,
this.scene.getElementsMapIncludingDeleted(),
this.scene,
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),

View file

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

View file

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

View file

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

View file

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

View file

@ -54,17 +54,13 @@ const handleDegreeChange: DragInputCallbackType<
if (!element) {
continue;
}
mutateElement(
element,
{
angle: nextAngle,
},
false,
);
mutateElement(element, {
angle: nextAngle,
});
const boundTextElement = getBoundTextElement(element, elementsMap);
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);
mutateElement(
latestElement,
{
angle: nextAngle,
},
false,
);
mutateElement(latestElement, {
angle: nextAngle,
});
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) {
mutateElement(boundTextElement, { angle: nextAngle }, false);
mutateElement(boundTextElement, { angle: nextAngle });
}
}
scene.triggerUpdate();

View file

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

View file

@ -84,19 +84,15 @@ const handleFontSizeChange: DragInputCallbackType<
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
for (const textElement of latestTextElements) {
mutateElement(
textElement,
{
fontSize: nextFontSize,
},
false,
);
mutateElement(textElement, {
fontSize: nextFontSize,
});
redrawTextBoundingBox(
textElement,
scene.getContainerElement(textElement),
elementsMap,
false,
(...args) => scene.mutateElement(...args),
);
}
@ -117,19 +113,15 @@ const handleFontSizeChange: DragInputCallbackType<
if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
}
mutateElement(
latestElement,
{
fontSize: nextFontSize,
},
false,
);
mutateElement(latestElement, {
fontSize: nextFontSize,
});
redrawTextBoundingBox(
latestElement,
scene.getContainerElement(latestElement),
elementsMap,
false,
(...args) => scene.mutateElement(...args),
);
}

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import {
bindOrUnbindLinearElements,
updateBoundElements,
} from "@excalidraw/element/binding";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { getBoundTextElement } from "@excalidraw/element/textElement";
import {
isFrameLikeElement,
@ -24,7 +23,6 @@ import type {
ElementsMap,
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
import type Scene from "../../scene/Scene";
@ -119,12 +117,11 @@ export const moveElement = (
newTopLeftX: number,
newTopLeftY: number,
originalElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
scene: Scene,
originalElementsMap: ElementsMap,
shouldInformMutation = true,
) => {
const elementsMap = scene.getNonDeletedElementsMap();
const latestElement = elementsMap.get(originalElement.id);
if (!latestElement) {
return;
@ -148,15 +145,15 @@ export const moveElement = (
-originalElement.angle as Radians,
);
mutateElement(
scene.mutateElement(
latestElement,
{
x,
y,
},
shouldInformMutation,
{ informMutation: shouldInformMutation },
);
updateBindings(latestElement, elementsMap, elements, scene);
updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement(
originalElement,
@ -165,13 +162,13 @@ export const moveElement = (
if (boundTextElement) {
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
latestBoundTextElement &&
mutateElement(
scene.mutateElement(
latestBoundTextElement,
{
x: boundTextElement.x + changeInX,
y: boundTextElement.y + changeInY,
},
shouldInformMutation,
{ informMutation: shouldInformMutation },
);
}
};
@ -199,8 +196,6 @@ export const getAtomicUnits = (
export const updateBindings = (
latestElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
scene: Scene,
options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[];
@ -209,16 +204,12 @@ export const updateBindings = (
},
) => {
if (isLinearElement(latestElement)) {
bindOrUnbindLinearElements(
[latestElement],
elementsMap,
elements,
scene,
true,
[],
options?.zoom,
);
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
} 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 { mutateElementWith } from "@excalidraw/element/mutateElement";
import { getCommonBounds } from "@excalidraw/element/bounds";
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
import type {
@ -63,8 +67,6 @@ import type {
import type { MarkOptional } from "@excalidraw/common/utility-types";
import { getCommonBounds } from "..";
export type ValidLinearElement = {
type: "arrow" | "line";
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;
};
@ -336,6 +343,7 @@ const bindLinearElementToElement = (
startBoundElement as ExcalidrawBindableElement,
"start",
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
);
}
}
@ -411,6 +419,7 @@ const bindLinearElementToElement = (
endBoundElement as ExcalidrawBindableElement,
"end",
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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