Remove mutators, pass scene everywhere, make temp scenes for special cases

This commit is contained in:
Marcel Mraz 2025-04-15 23:24:59 +02:00
parent 567c9f51e4
commit acfa33650e
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
33 changed files with 177 additions and 266 deletions

View file

@ -34,6 +34,7 @@ import type {
NonDeletedSceneElementsMap,
OrderedExcalidrawElement,
Ordered,
ElementsMap,
} from "@excalidraw/element/types";
import type {
@ -42,7 +43,7 @@ import type {
SameType,
} from "@excalidraw/common/utility-types";
import type { AppState } from "../types";
import type { AppState } from "../../excalidraw/types";
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
@ -166,6 +167,12 @@ class Scene {
return this.frames;
}
constructor(elementsMap: ElementsMap | null = null) {
if (elementsMap) {
this.replaceAllElements(elementsMap);
}
}
getSelectedElements(opts: {
// NOTE can be ommitted by making Scene constructor require App instance
selectedElementIds: AppState["selectedElementIds"];
@ -419,7 +426,6 @@ 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 it either way)
// Mutate an element with passed updates and trigger the component to update. Make sure you
// are calling it either from a React event handler or within unstable_batchedUpdates().
mutate<TElement extends Mutable<ExcalidrawElement>>(

View file

@ -1,9 +1,9 @@
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds";
import { getMaximumGroups } from "./groups";
import type Scene from "./Scene";
import type { BoundingBox } from "./bounds";
import type { ExcalidrawElement } from "./types";
@ -38,7 +38,7 @@ export const alignElements = (
});
// update bound elements
updateBoundElements(element, elementsMap, {
updateBoundElements(element, scene, {
simultaneouslyUpdated: group,
});
return updatedEle;

View file

@ -30,8 +30,6 @@ import { isPointOnShape } from "@excalidraw/utils/collision";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { AppState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
@ -68,6 +66,8 @@ import {
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
import type Scene from "./Scene";
import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement";
import type {
@ -177,8 +177,6 @@ const bindOrUnbindLinearElementEdge = (
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
scene: Scene,
): void => {
const elementsMap = scene.getNonDeletedElementsMap();
// "keep" is for method chaining convenience, a "no-op", so just bail out
if (bindableElement === "keep") {
return;
@ -209,23 +207,11 @@ const bindOrUnbindLinearElementEdge = (
: startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id)
) {
bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
(...args) => scene.mutate(...args),
);
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
boundToElementIds.add(bindableElement.id);
}
} else {
bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
(...args) => scene.mutate(...args),
);
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
boundToElementIds.add(bindableElement.id);
}
};
@ -443,8 +429,7 @@ export const maybeBindLinearElement = (
linearElement,
appState.startBoundElement,
"start",
elementsMap,
(...args) => scene.mutate(...args),
scene,
);
}
@ -465,13 +450,7 @@ export const maybeBindLinearElement = (
"end",
)
) {
bindLinearElement(
linearElement,
hoveredElement,
"end",
elementsMap,
(...args) => scene.mutate(...args),
);
bindLinearElement(linearElement, hoveredElement, "end", scene);
}
}
};
@ -500,11 +479,7 @@ export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
mutator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => ExcalidrawElement,
scene: Scene,
): void => {
if (!isArrowElement(linearElement)) {
return;
@ -517,7 +492,7 @@ export const bindLinearElement = (
linearElement,
hoveredElement,
startOrEnd,
elementsMap as NonDeletedSceneElementsMap,
scene.getNonDeletedElementsMap(),
),
hoveredElement,
),
@ -534,13 +509,13 @@ export const bindLinearElement = (
};
}
mutator(linearElement, {
scene.mutate(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
});
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
if (!boundElementsMap.has(linearElement.id)) {
mutator(hoveredElement, {
scene.mutate(hoveredElement, {
boundElements: (hoveredElement.boundElements || []).concat({
id: linearElement.id,
type: "arrow",
@ -757,7 +732,7 @@ const calculateFocusAndGap = (
// in explicitly.
export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
scene: Scene,
options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number };
@ -773,6 +748,8 @@ export const updateBoundElements = (
return;
}
const elementsMap = scene.getNonDeletedElementsMap();
boundElementsVisitor(elementsMap, changedElement, (element) => {
if (!isLinearElement(element) || element.isDeleted) {
return;
@ -860,7 +837,7 @@ export const updateBoundElements = (
}> => update !== null,
);
LinearElementEditor.movePoints(element, elementsMap, updates, {
LinearElementEditor.movePoints(element, scene, updates, {
...(changedElement.id === element.startBinding?.elementId
? { startBinding: bindings.startBinding }
: {}),

View file

@ -11,8 +11,6 @@ import type {
PointerDownState,
} from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import { updateBoundElements } from "./binding";
@ -28,6 +26,8 @@ import {
isTextElement,
} from "./typeChecks";
import type Scene from "./Scene";
import type { Bounds } from "./bounds";
import type { ExcalidrawElement } from "./types";
@ -118,7 +118,7 @@ export const dragSelectedElements = (
adjustedOffset,
);
}
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
updateBoundElements(element, scene, {
simultaneouslyUpdated: Array.from(elementsToUpdate),
});
}

View file

@ -7,8 +7,6 @@ import type {
PendingExcalidrawElements,
} from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { bindLinearElement } from "./binding";
import { updateElbowArrowPoints } from "./elbowArrow";
import {
@ -41,6 +39,8 @@ import {
type OrderedExcalidrawElement,
} from "./types";
import type Scene from "./Scene";
type LinkDirection = "up" | "right" | "down" | "left";
const VERTICAL_OFFSET = 100;
@ -445,20 +445,8 @@ const createBindingArrow = (
const elementsMap = scene.getNonDeletedElementsMap();
bindLinearElement(
bindingArrow,
startBindingElement,
"start",
elementsMap,
(...args) => scene.mutate(...args),
);
bindLinearElement(
bindingArrow,
endBindingElement,
"end",
elementsMap,
(...args) => scene.mutate(...args),
);
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
const changedElements = new Map<string, OrderedExcalidrawElement>();
changedElements.set(
@ -474,7 +462,7 @@ const createBindingArrow = (
bindingArrow as OrderedExcalidrawElement,
);
LinearElementEditor.movePoints(bindingArrow, elementsMap, [
LinearElementEditor.movePoints(bindingArrow, scene, [
{
index: 1,
point: bindingArrow.points[1],

View file

@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
import type {
AppClassProperties,
AppState,
@ -29,6 +27,8 @@ import {
isTextElement,
} from "./typeChecks";
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
import type {
ElementsMap,
ElementsMapOrArray,

View file

@ -20,8 +20,6 @@ import {
tupleToCoors,
} from "@excalidraw/common";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { Store } from "@excalidraw/excalidraw/store";
import type { Radians } from "@excalidraw/math";
@ -69,6 +67,8 @@ import {
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
import type Scene from "./Scene";
import type { Bounds } from "./bounds";
import type {
NonDeleted,
@ -80,7 +80,6 @@ import type {
ElementsMap,
NonDeletedSceneElementsMap,
FixedPointBinding,
SceneElementsMap,
FixedSegment,
ExcalidrawElbowArrowElement,
} from "./types";
@ -307,7 +306,7 @@ export class LinearElementEditor {
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
LinearElementEditor.movePoints(element, elementsMap, [
LinearElementEditor.movePoints(element, scene, [
{
index: selectedIndex,
point: pointFrom(
@ -331,7 +330,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints(
element,
elementsMap,
scene,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition: LocalPoint =
pointIndex === lastClickedPoint
@ -452,7 +451,7 @@ export class LinearElementEditor {
selectedPoint === element.points.length - 1
) {
if (isPathALoop(element.points, appState.zoom.value)) {
LinearElementEditor.movePoints(element, elementsMap, [
LinearElementEditor.movePoints(element, scene, [
{
index: selectedPoint,
point:
@ -932,13 +931,13 @@ export class LinearElementEditor {
scenePointerX: number,
scenePointerY: number,
app: AppClassProperties,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): LinearElementEditor | null {
const appState = app.state;
if (!appState.editingLinearElement) {
return null;
}
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return appState.editingLinearElement;
@ -949,7 +948,7 @@ export class LinearElementEditor {
if (!event.altKey) {
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, elementsMap, [
LinearElementEditor.deletePoints(element, app.scene, [
points.length - 1,
]);
}
@ -989,16 +988,14 @@ export class LinearElementEditor {
}
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoints(element, elementsMap, [
LinearElementEditor.movePoints(element, app.scene, [
{
index: element.points.length - 1,
point: newPoint,
},
]);
} else {
LinearElementEditor.addPoints(element, elementsMap, [
{ point: newPoint },
]);
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]);
}
return {
...appState.editingLinearElement,
@ -1168,7 +1165,6 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
) {
// TODO_SCENE: we don't need to inform mutation here?
mutateElementWith(
element,
elementsMap,
@ -1231,7 +1227,7 @@ export class LinearElementEditor {
// potentially expanding the bounding box
if (pointAddedToEnd) {
const lastPoint = element.points[element.points.length - 1];
LinearElementEditor.movePoints(element, elementsMap, [
LinearElementEditor.movePoints(element, scene, [
{
index: element.points.length - 1,
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
@ -1250,7 +1246,7 @@ export class LinearElementEditor {
static deletePoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scene: Scene,
pointIndices: readonly number[],
) {
let offsetX = 0;
@ -1283,7 +1279,7 @@ export class LinearElementEditor {
LinearElementEditor._updatePoints(
element,
elementsMap,
scene,
nextPoints,
offsetX,
offsetY,
@ -1292,7 +1288,7 @@ export class LinearElementEditor {
static addPoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scene: Scene,
targetPoints: { point: LocalPoint }[],
) {
const offsetX = 0;
@ -1301,7 +1297,7 @@ export class LinearElementEditor {
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
LinearElementEditor._updatePoints(
element,
elementsMap,
scene,
nextPoints,
offsetX,
offsetY,
@ -1310,7 +1306,7 @@ export class LinearElementEditor {
static movePoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scene: Scene,
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
otherUpdates?: {
startBinding?: PointBinding | null;
@ -1349,7 +1345,7 @@ export class LinearElementEditor {
LinearElementEditor._updatePoints(
element,
elementsMap,
scene,
nextPoints,
offsetX,
offsetY,
@ -1462,7 +1458,7 @@ export class LinearElementEditor {
private static _updatePoints(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scene: Scene,
nextPoints: readonly LocalPoint[],
offsetX: number,
offsetY: number,
@ -1499,7 +1495,7 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints);
mutateElementWith(element, elementsMap, updates, {
scene.mutate(element, updates, {
isDragging: options?.isDragging,
});
} else {
@ -1516,7 +1512,7 @@ export class LinearElementEditor {
pointFrom(dX, dY),
element.angle,
);
mutateElement(element, {
scene.mutate(element, {
...otherUpdates,
points: nextPoints,
x: element.x + rotated[0],

View file

@ -17,8 +17,6 @@ import {
import type { GlobalPoint } from "@excalidraw/math";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { PointerDownState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
@ -60,6 +58,8 @@ import {
import { isInGroup } from "./groups";
import type Scene from "./Scene";
import type { BoundingBox } from "./bounds";
import type {
MaybeTransformHandleType,
@ -103,7 +103,7 @@ export const transformElements = (
pointerY,
shouldRotateWithDiscreteAngle,
);
updateBoundElements(element, elementsMap);
updateBoundElements(element, scene);
}
} else if (isTextElement(element) && transformHandleType) {
resizeSingleTextElement(
@ -115,7 +115,7 @@ export const transformElements = (
pointerX,
pointerY,
);
updateBoundElements(element, elementsMap);
updateBoundElements(element, scene);
return true;
} else if (transformHandleType) {
const elementId = selectedElements[0].id;
@ -554,7 +554,7 @@ const rotateMultipleElements = (
scene.mutate(element, updates);
updateBoundElements(element, elementsMap, {
updateBoundElements(element, scene, {
simultaneouslyUpdated: elements,
});
@ -964,7 +964,7 @@ export const resizeSingleElement = (
const elementsMap = scene.getNonDeletedElementsMap();
updateBoundElements(latestElement, elementsMap, {
updateBoundElements(latestElement, scene, {
// TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight },
});
@ -1525,7 +1525,7 @@ export const resizeMultipleElements = (
isDragging: true,
});
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
updateBoundElements(element, scene, {
simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height },
});

View file

@ -27,7 +27,7 @@ import {
isTextElement,
} from "./typeChecks";
import type { ElementUpdate } from "./mutateElement";
import type Scene from "./Scene";
import type { MaybeTransformHandleType } from "./transformHandles";
import type {
@ -43,12 +43,10 @@ import type {
export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null,
elementsMap: ElementsMap,
mutator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => ExcalidrawElement,
scene: Scene,
) => {
const elementsMap = scene.getNonDeletedElementsMap();
let maxWidth = undefined;
const boundTextUpdates = {
x: textElement.x,
@ -96,30 +94,34 @@ export const redrawTextBoundingBox = (
metrics.height,
container.type,
);
mutator(container, { height: nextHeight });
scene.mutate(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight);
}
if (metrics.width > maxContainerWidth) {
const nextWidth = computeContainerDimensionForBoundText(
metrics.width,
container.type,
);
mutator(container, { width: nextWidth });
scene.mutate(container, { width: nextWidth });
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(
container,
updatedTextElement,
elementsMap,
);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
}
mutator(textElement, boundTextUpdates);
scene.mutate(textElement, boundTextUpdates);
};
export const handleBindTextResize = (

View file

@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
import type { AppState } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { isFrameLikeElement } from "./typeChecks";
import { getElementsInGroup } from "./groups";
@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
import { getSelectedElements } from "./selection";
import type Scene from "./Scene";
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {

View file

@ -2,7 +2,6 @@ import { ARROW_TYPE } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math";
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
import Scene from "@excalidraw/excalidraw/scene/Scene";
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
import { bindLinearElement } from "../src/binding";
import Scene from "../src/Scene";
import type {
ExcalidrawArrowElement,
ExcalidrawBindableElement,
@ -187,13 +188,9 @@ describe("elbow arrow routing", () => {
scene.insertElement(rectangle1);
scene.insertElement(rectangle2);
scene.insertElement(arrow);
const elementsMap = scene.getNonDeletedElementsMap();
bindLinearElement(arrow, rectangle1, "start", elementsMap, (...args) =>
scene.mutate(...args),
);
bindLinearElement(arrow, rectangle2, "end", elementsMap, (...args) =>
scene.mutate(...args),
);
bindLinearElement(arrow, rectangle1, "start", scene);
bindLinearElement(arrow, rectangle2, "end", scene);
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null);

View file

@ -162,12 +162,7 @@ export const actionBindText = register({
}),
});
const originalContainerHeight = container.height;
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
redrawTextBoundingBox(textElement, container, app.scene);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
@ -312,12 +307,8 @@ export const actionWrapTextInContainer = register({
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
});
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
redrawTextBoundingBox(textElement, container, app.scene);
updatedElements = pushContainerBelowText(
[...updatedElements, container],

View file

@ -259,7 +259,7 @@ export const actionDeleteSelected = register({
LinearElementEditor.deletePoints(
element,
elementsMap,
app.scene,
selectedPointsIndices,
);

View file

@ -68,6 +68,8 @@ import type {
VerticalAlign,
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { trackEvent } from "../analytics";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
@ -135,7 +137,6 @@ 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;
@ -246,8 +247,7 @@ const changeFontSize = (
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
app.scene,
);
newElement = offsetElementAfterFontResize(
@ -264,12 +264,11 @@ const changeFontSize = (
);
// Update arrow elements after text elements have been updated
const updatedElementsMap = arrayToMap(updatedElements);
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
}).forEach((element) => {
if (isTextElement(element)) {
updateBoundElements(element, updatedElementsMap);
updateBoundElements(element, app.scene);
}
});
@ -947,12 +946,7 @@ export const actionChangeFontFamily = register({
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
for (const [element, container] of elementContainerMapping) {
// trigger synchronous redraw
redrawTextBoundingBox(
element,
container,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
redrawTextBoundingBox(element, container, app.scene);
}
} else {
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
@ -969,8 +963,7 @@ export const actionChangeFontFamily = register({
redrawTextBoundingBox(
latestElement as ExcalidrawTextElement,
latestContainer,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
app.scene,
);
}
}
@ -1176,8 +1169,7 @@ export const actionChangeTextAlign = register({
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
app.scene,
);
return newElement;
}
@ -1268,8 +1260,7 @@ export const actionChangeVerticalAlign = register({
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
app.scene,
);
return newElement;
}
@ -1669,17 +1660,10 @@ export const actionChangeArrowType = register({
newElement,
startHoveredElement,
"start",
elementsMap,
(...args) => app.scene.mutate(...args),
app.scene,
);
endHoveredElement &&
bindLinearElement(
newElement,
endHoveredElement,
"end",
elementsMap,
(...args) => app.scene.mutate(...args),
);
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
const startBinding =
startElement && newElement.startBinding
@ -1733,13 +1717,7 @@ export const actionChangeArrowType = register({
newElement.startBinding.elementId,
) as ExcalidrawBindableElement;
if (startElement) {
bindLinearElement(
newElement,
startElement,
"start",
elementsMap,
(...args) => app.scene.mutate(...args),
);
bindLinearElement(newElement, startElement, "start", app.scene);
}
}
if (newElement.endBinding) {
@ -1747,13 +1725,7 @@ export const actionChangeArrowType = register({
newElement.endBinding.elementId,
) as ExcalidrawBindableElement;
if (endElement) {
bindLinearElement(
newElement,
endElement,
"end",
elementsMap,
(...args) => app.scene.mutate(...args),
);
bindLinearElement(newElement, endElement, "end", app.scene);
}
}
}

View file

@ -139,12 +139,8 @@ export const actionPasteStyles = register({
element.id === newElement.containerId,
) || null;
}
redrawTextBoundingBox(
newElement,
container,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
redrawTextBoundingBox(newElement, container, app.scene);
}
if (

View file

@ -16,7 +16,6 @@ import {
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
mutateElement,
mutateElementWith,
newElementWith,
} from "@excalidraw/element/mutateElement";
import {
@ -38,6 +37,8 @@ import {
syncMovedIndices,
} from "@excalidraw/element/fractionalIndex";
import Scene from "@excalidraw/element/Scene";
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
@ -1135,8 +1136,13 @@ export class ElementsChange implements Change<SceneElementsMap> {
}
try {
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
// we also don't have a scene on the server
// so we are creating a temp scene just to query and mutate elements
const tempScene = new Scene(nextElements);
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
ElementsChange.redrawTextBoundingBoxes(tempScene, changedElements);
// the following reorder performs also mutations, but only on new instances of changed elements
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
@ -1147,7 +1153,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
);
// Need ordered nextElements to avoid z-index binding issues
ElementsChange.redrawBoundArrows(nextElements, changedElements);
ElementsChange.redrawBoundArrows(tempScene, changedElements);
} catch (e) {
console.error(
`Couldn't mutate elements after applying elements change`,
@ -1459,9 +1465,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
}
private static redrawTextBoundingBoxes(
elements: SceneElementsMap,
scene: Scene,
changed: Map<string, OrderedExcalidrawElement>,
) {
const elements = scene.getNonDeletedElementsMap();
const boxesToRedraw = new Map<
string,
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
@ -1501,22 +1508,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
continue;
}
redrawTextBoundingBox(
boundText,
container,
elements,
(element, updates) => mutateElementWith(element, elements, updates),
);
redrawTextBoundingBox(boundText, container, scene);
}
}
private static redrawBoundArrows(
elements: SceneElementsMap,
scene: Scene,
changed: Map<string, OrderedExcalidrawElement>,
) {
for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) {
updateBoundElements(element, elements, {
updateBoundElements(element, scene, {
changedElements: changed,
});
}

View file

@ -299,6 +299,8 @@ import {
import { isNonDeletedElement } from "@excalidraw/element";
import Scene from "@excalidraw/element/Scene";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
@ -400,7 +402,6 @@ import {
hasBackground,
isSomeElementSelected,
} from "../scene";
import Scene from "../scene/Scene";
import { getStateForZoom } from "../scene/zoom";
import {
dataURLToFile,
@ -3332,12 +3333,7 @@ class App extends React.Component<AppProps, AppState> {
newElement,
this.scene.getElementsMapIncludingDeleted(),
);
redrawTextBoundingBox(
newElement,
container,
this.scene.getElementsMapIncludingDeleted(),
(...args) => this.scene.mutate(...args),
);
redrawTextBoundingBox(newElement, container, this.scene);
}
});
@ -4439,7 +4435,7 @@ class App extends React.Component<AppProps, AppState> {
{ informMutation: false },
);
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
updateBoundElements(element, this.scene, {
simultaneouslyUpdated: selectedElements,
});
});
@ -4976,7 +4972,7 @@ class App extends React.Component<AppProps, AppState> {
onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element, this.scene.getNonDeletedElementsMap());
updateBoundElements(element, this.scene);
}
}),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
@ -5883,7 +5879,6 @@ class App extends React.Component<AppProps, AppState> {
scenePointerX,
scenePointerY,
this,
this.scene.getNonDeletedElementsMap(),
);
if (
@ -10762,16 +10757,12 @@ class App extends React.Component<AppProps, AppState> {
),
);
updateBoundElements(
croppingElement,
this.scene.getNonDeletedElementsMap(),
{
newSize: {
width: croppingElement.width,
height: croppingElement.height,
},
updateBoundElements(croppingElement, this.scene, {
newSize: {
width: croppingElement.width,
height: croppingElement.height,
},
);
});
this.setState({
isCropping: transformHandleType && transformHandleType !== "rotation",

View file

@ -7,13 +7,14 @@ import type { Degrees } from "@excalidraw/math";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { angleIcon } from "../icons";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface AngleProps {

View file

@ -1,9 +1,10 @@
import type Scene from "@excalidraw/element/Scene";
import { getNormalizedGridStep } from "../../scene";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface PositionProps {

View file

@ -10,11 +10,12 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface DimensionDragInputProps {

View file

@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { CaptureUpdateAction } from "../../store";
import { useApp } from "../App";
import { InlineIcon } from "../InlineIcon";
@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
import "./DragInput.scss";
import type { StatsInputProperty } from "./utils";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
export type DragInputCallbackType<

View file

@ -13,13 +13,14 @@ import type {
ExcalidrawTextElement,
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { fontSizeIcon } from "../icons";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface FontSizeProps {
@ -74,8 +75,7 @@ const handleFontSizeChange: DragInputCallbackType<
redrawTextBoundingBox(
latestElement,
scene.getContainerElement(latestElement),
scene.getNonDeletedElementsMap(),
(...args) => scene.mutate(...args),
scene,
);
}
}

View file

@ -11,13 +11,14 @@ import type { Degrees } from "@excalidraw/math";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { angleIcon } from "../icons";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface MultiAngleProps {

View file

@ -16,24 +16,20 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
import { getCommonBounds } from "@excalidraw/utils";
import {
mutateElement,
mutateElementWith,
} from "@excalidraw/element/mutateElement";
import type {
ElementsMap,
ExcalidrawElement,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import DragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
import { getElementsInAtomicUnit } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type { AtomicUnit } from "./utils";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface MultiDimensionProps {
@ -79,12 +75,13 @@ const resizeElementInGroup = (
scale: number,
latestElement: ExcalidrawElement,
origElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
) => {
const elementsMap = scene.getNonDeletedElementsMap();
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
mutateElementWith(latestElement, elementsMap, updates);
scene.mutate(latestElement, updates);
const boundTextElement = getBoundTextElement(
origElement,
@ -92,12 +89,12 @@ const resizeElementInGroup = (
);
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, {
updateBoundElements(latestElement, scene, {
newSize: { width: updates.width, height: updates.height },
});
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
mutateElement(latestBoundTextElement, {
scene.mutate(latestBoundTextElement, {
fontSize: newFontSize,
});
handleBindTextResize(
@ -119,8 +116,8 @@ const resizeGroup = (
property: MultiDimensionProps["property"],
latestElements: ExcalidrawElement[],
originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
) => {
// keep aspect ratio for groups
if (property === "width") {
@ -142,8 +139,8 @@ const resizeGroup = (
scale,
latestElement,
origElement,
elementsMap,
originalElementsMap,
scene,
);
}
};
@ -195,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
property,
latestElements,
originalElements,
elementsMap,
originalElementsMap,
scene,
);
} else {
const [el] = elementsInUnit;
@ -302,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
property,
latestElements,
originalElements,
elementsMap,
originalElementsMap,
scene,
);
} else {
const [el] = elementsInUnit;

View file

@ -16,13 +16,14 @@ import type {
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { fontSizeIcon } from "../icons";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface MultiFontSizeProps {
@ -91,8 +92,7 @@ const handleFontSizeChange: DragInputCallbackType<
redrawTextBoundingBox(
textElement,
scene.getContainerElement(textElement),
elementsMap,
(...args) => scene.mutate(...args),
scene,
);
}
@ -120,8 +120,7 @@ const handleFontSizeChange: DragInputCallbackType<
redrawTextBoundingBox(
latestElement,
scene.getContainerElement(latestElement),
elementsMap,
(...args) => scene.mutate(...args),
scene,
);
}

View file

@ -7,13 +7,14 @@ import { getCommonBounds } from "@excalidraw/element/bounds";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import StatsDragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
import { getElementsInAtomicUnit, moveElement } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type { AtomicUnit } from "./utils";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface MultiPositionProps {

View file

@ -9,11 +9,12 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import StatsDragInput from "./DragInput";
import { getStepSizedValue, moveElement } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface PositionProps {

View file

@ -25,7 +25,8 @@ import type {
NonDeletedExcalidrawElement,
} from "@excalidraw/element/types";
import type Scene from "../../scene/Scene";
import type Scene from "@excalidraw/element/Scene";
import type { AppState } from "../../types";
export type StatsInputProperty =
@ -206,10 +207,6 @@ export const updateBindings = (
if (isLinearElement(latestElement)) {
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
} else {
updateBoundElements(
latestElement,
scene.getNonDeletedElementsMap(),
options,
);
updateBoundElements(latestElement, scene, options);
}
};

View file

@ -38,14 +38,13 @@ 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 Scene from "@excalidraw/element/Scene";
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
import type {
ElementsMap,
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElement,
@ -223,7 +222,7 @@ const DEFAULT_DIMENSION = 100;
const bindTextToContainer = (
container: ExcalidrawElement,
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
elementsMap: ElementsMap,
scene: Scene,
) => {
const textElement: ExcalidrawTextElement = newTextElement({
x: 0,
@ -242,12 +241,8 @@ const bindTextToContainer = (
}),
});
redrawTextBoundingBox(
textElement,
container,
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
);
redrawTextBoundingBox(textElement, container, scene);
return [container, textElement] as const;
};
@ -256,7 +251,7 @@ const bindLinearElementToElement = (
start: ValidLinearElement["start"],
end: ValidLinearElement["end"],
elementStore: ElementStore,
elementsMap: NonDeletedSceneElementsMap,
scene: Scene,
): {
linearElement: ExcalidrawLinearElement;
startBoundElement?: ExcalidrawElement;
@ -342,8 +337,7 @@ const bindLinearElementToElement = (
linearElement,
startBoundElement as ExcalidrawBindableElement,
"start",
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
scene,
);
}
}
@ -418,8 +412,7 @@ const bindLinearElementToElement = (
linearElement,
endBoundElement as ExcalidrawBindableElement,
"end",
elementsMap,
(element, updates) => mutateElementWith(element, elementsMap, updates),
scene,
);
}
}
@ -660,6 +653,9 @@ export const convertToExcalidrawElements = (
}
const elementsMap = elementStore.getElementsMap();
// we don't have a real scene, so we just use a temp scene to query and mutate elements
const scene = new Scene(elementsMap);
// Add labels and arrow bindings
for (const [id, element] of elementsWithIds) {
const excalidrawElement = elementStore.getElement(id)!;
@ -673,7 +669,7 @@ export const convertToExcalidrawElements = (
let [container, text] = bindTextToContainer(
excalidrawElement,
element?.label,
elementsMap,
scene,
);
elementStore.add(container);
elementStore.add(text);
@ -701,7 +697,7 @@ export const convertToExcalidrawElements = (
originalStart,
originalEnd,
elementStore,
elementsMap,
scene,
);
container = linearElement;
elementStore.add(linearElement);
@ -726,7 +722,7 @@ export const convertToExcalidrawElements = (
start,
end,
elementStore,
elementsMap,
scene,
);
elementStore.add(linearElement);

View file

@ -28,6 +28,8 @@ import type {
import type { ValueOf } from "@excalidraw/common/utility-types";
import type Scene from "@excalidraw/element/Scene";
import { CascadiaFontFaces } from "./Cascadia";
import { ComicShannsFontFaces } from "./ComicShanns";
import { EmojiFontFaces } from "./Emoji";
@ -40,8 +42,6 @@ import { NunitoFontFaces } from "./Nunito";
import { VirgilFontFaces } from "./Virgil";
import { XiaolaiFontFaces } from "./Xiaolai";
import type Scene from "../scene/Scene";
export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use
// a static member to reduce memory footprint

View file

@ -9,10 +9,11 @@ import type {
NonDeletedExcalidrawElement,
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/element/Scene";
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
import { renderStaticSceneThrottled } from "../renderer/staticScene";
import type Scene from "./Scene";
import type { RenderableElementsMap } from "./types";
import type { AppState } from "../types";

View file

@ -1384,7 +1384,7 @@ describe("Test Linear Elements", () => {
const [origStartX, origStartY] = [line.x, line.y];
act(() => {
LinearElementEditor.movePoints(line, arrayToMap(h.elements), [
LinearElementEditor.movePoints(line, h.app.scene, [
{
index: 0,
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),

View file

@ -577,12 +577,8 @@ export const textWysiwyg = ({
),
});
}
redrawTextBoundingBox(
updateElement,
container,
app.scene.getNonDeletedElementsMap(),
(...args) => app.scene.mutate(...args),
);
redrawTextBoundingBox(updateElement, container, app.scene);
}
onSubmit({