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

@ -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.mutate(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.mutate(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.mutate(...args),
);
boundToElementIds.add(bindableElement.id);
}
} else {
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...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.mutate(...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.mutate(...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.mutate(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.mutate(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.mutate(
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.mutate(...args),
);
bindLinearElement(
bindingArrow,
endBindingElement,
"end",
elementsMap as NonDeletedSceneElementsMap,
scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...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

@ -21,7 +21,7 @@ import {
getCommonBounds,
getElementAbsoluteCoords,
} from "./bounds";
import { mutateElement } from "./mutateElement";
import { mutateElementWith } from "./mutateElement";
import { getBoundTextElement, getContainerElement } from "./textElement";
import {
isFrameElement,
@ -57,13 +57,9 @@ export const bindElementsToFramesAfterDuplication = (
if (nextElementId) {
const nextElement = nextElementMap.get(nextElementId);
if (nextElement) {
mutateElement(
nextElement,
{
frameId: nextFrameId ?? element.frameId,
},
false,
);
mutateElementWith(nextElement, nextElementMap, {
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,
);
mutateElementWith(element, elementsMap, {
frameId: frame.id,
});
}
return allElements;
@ -611,13 +603,9 @@ export const removeElementsFromFrame = (
}
for (const [, element] of _elementsToRemove) {
mutateElement(
element,
{
frameId: null,
},
false,
);
mutateElementWith(element, elementsMap, {
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;
@ -792,11 +792,8 @@ export class LinearElementEditor {
elementsMap,
);
} else if (event.altKey && appState.editingLinearElement) {
if (
linearElementEditor.lastUncommittedPoint == null &&
!isElbowArrow(element)
) {
mutateElement(element, {
if (linearElementEditor.lastUncommittedPoint == null) {
scene.mutate(element, {
points: [
...element.points,
LinearElementEditor.createPointAt(
@ -862,7 +859,6 @@ export class LinearElementEditor {
element,
startBindingElement,
endBindingElement,
elementsMap,
scene,
);
}
@ -1161,23 +1157,27 @@ 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,
) {
// TODO_SCENE: we don't need to inform mutation here?
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 +1220,7 @@ export class LinearElementEditor {
return acc;
}, []);
const updates = { points: nextPoints };
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
scene.mutate(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 +1395,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 +1427,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.mutate(element, { points });
ret.pointerDownState = {
...linearElementEditor.pointerDownState,
@ -1488,28 +1479,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 +1763,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 +1808,9 @@ export class LinearElementEditor {
.map((segment) => segment.index)
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
mutateElbowArrow(
element,
{
fixedSegments: nextFixedSegments,
},
true,
elementsMap,
);
scene.mutate(element, {
fixedSegments: nextFixedSegments,
});
const point = pointFrom<GlobalPoint>(
element.x +
@ -1873,19 +1842,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.mutate(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.mutate`.
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>>(
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 } 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,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@ -131,8 +127,6 @@ export const transformElements = (
getNextSingleWidthAndHeightFromPointer(
latestElement,
origElement,
elementsMap,
originalElements,
transformHandleType,
pointerX,
pointerY,
@ -147,8 +141,8 @@ export const transformElements = (
nextHeight,
latestElement,
origElement,
elementsMap,
originalElements,
scene,
transformHandleType,
{
shouldMaintainAspectRatio,
@ -163,7 +157,6 @@ export const transformElements = (
rotateMultipleElements(
originalElements,
selectedElements,
elementsMap,
scene,
pointerX,
pointerY,
@ -212,13 +205,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 +230,13 @@ const rotateSingleElement = (
}
const boundTextElementId = getBoundTextElementId(element);
mutateElement(element, { angle });
scene.mutate(element, { angle });
if (boundTextElementId) {
const textElement =
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
if (textElement && !isArrowElement(element)) {
mutateElement(textElement, { angle });
scene.mutate(textElement, { angle });
}
}
};
@ -291,12 +286,13 @@ export const measureFontSizeFromWidth = (
const resizeSingleTextElement = (
originalElements: PointerDownState["originalElements"],
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
scene: Scene,
transformHandleType: TransformHandleDirection,
shouldResizeFromCenter: boolean,
pointerX: number,
pointerY: number,
) => {
const elementsMap = scene.getNonDeletedElementsMap();
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
@ -395,7 +391,7 @@ const resizeSingleTextElement = (
);
const [nextX, nextY] = newTopLeft;
mutateElement(element, {
scene.mutate(element, {
fontSize: metrics.size,
width: nextWidth,
height: nextHeight,
@ -510,14 +506,13 @@ const resizeSingleTextElement = (
autoResize: false,
};
mutateElement(element, resizedElement);
scene.mutate(element, resizedElement);
}
};
const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene,
pointerX: number,
pointerY: number,
@ -525,6 +520,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) {
@ -545,27 +541,18 @@ const rotateMultipleElements = (
(centerAngle + origAngle - element.angle) as Radians,
);
if (isElbowArrow(element)) {
// Needed to re-route the arrow
mutateElbowArrow(
element,
{
const updates = isElbowArrow(element)
? {
// Needed to re-route the arrow
points: getArrowLocalFixedPoints(element, elementsMap),
},
false,
elementsMap,
);
} else {
mutateElement(
element,
{
}
: {
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
}
};
scene.mutate(element, updates);
updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements,
@ -573,15 +560,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,
);
mutateElementWith(boundText, elementsMap, {
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeRadians((centerAngle + origAngle) as Radians),
});
}
}
}
@ -826,8 +809,8 @@ export const resizeSingleElement = (
nextHeight: number,
latestElement: ExcalidrawElement,
origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
handleDirection: TransformHandleDirection,
{
shouldInformMutation = true,
@ -840,6 +823,7 @@ export const resizeSingleElement = (
} = {},
) => {
let boundTextFont: { fontSize?: number } = {};
const elementsMap = scene.getNonDeletedElementsMap();
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement) {
@ -939,7 +923,7 @@ export const resizeSingleElement = (
}
if ("scale" in latestElement && "scale" in origElement) {
mutateElement(latestElement, {
scene.mutate(latestElement, {
scale: [
// defaulting because scaleX/Y can be 0/-0
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
@ -974,15 +958,19 @@ export const resizeSingleElement = (
...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
newSize: { width: nextWidth, height: nextHeight },
});
if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, {
scene.mutate(boundTextElement, {
fontSize: boundTextFont.fontSize,
});
}
@ -998,8 +986,6 @@ export const resizeSingleElement = (
const getNextSingleWidthAndHeightFromPointer = (
latestElement: ExcalidrawElement,
origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap,
handleDirection: TransformHandleDirection,
pointerX: number,
pointerY: number,
@ -1534,26 +1520,22 @@ export const resizeMultipleElements = (
} of elementsAndUpdates) {
const { width, height, angle } = update;
scene.mutateElement(element, update, false, {
scene.mutate(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.mutate(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.mutate(...args),
);
bindLinearElement(arrow, rectangle2, "end", elementsMap, (...args) =>
scene.mutate(...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,