refactor: remove dependency on the (static) Scene (#9389)

This commit is contained in:
Marcel Mraz
2025-04-23 13:45:08 +02:00
committed by GitHub
parent debf2ad608
commit 1913599594
67 changed files with 812 additions and 925 deletions
+1 -7
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);
+20 -29
View File
@@ -27,7 +27,6 @@ import {
isUsingAdaptiveRadius,
} from "@excalidraw/element/typeChecks";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { measureText } from "@excalidraw/element/textMeasurements";
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
@@ -43,12 +42,12 @@ import type {
import type { Mutable } from "@excalidraw/common/utility-types";
import type { Radians } from "@excalidraw/math";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
import type { Radians } from "../../math/src";
import type { AppState } from "../types";
export const actionUnbindText = register({
@@ -80,7 +79,7 @@ export const actionUnbindText = register({
boundTextElement,
elementsMap,
);
mutateElement(boundTextElement as ExcalidrawTextElement, {
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null,
width,
height,
@@ -88,7 +87,7 @@ export const actionUnbindText = register({
x,
y,
});
mutateElement(element, {
app.scene.mutateElement(element, {
boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id,
),
@@ -153,25 +152,21 @@ 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,
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
});
mutateElement(container, {
app.scene.mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: textElement.id,
}),
});
const originalContainerHeight = container.height;
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
redrawTextBoundingBox(textElement, container, app.scene);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
@@ -301,27 +296,23 @@ export const actionWrapTextInContainer = register({
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, 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,
);
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
app.scene.mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
});
redrawTextBoundingBox(textElement, container, app.scene);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
@@ -1,6 +1,6 @@
import React from "react";
import { Excalidraw, mutateElement } from "../index";
import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api";
import { act, assertElements, render } from "../tests/test-utils";
@@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: f1.id,
});
mutateElement(r1, {
h.app.scene.mutateElement(r1, {
boundElements: [{ type: "text", id: t1.id }],
});
@@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null,
});
mutateElement(r1, {
h.app.scene.mutateElement(r1, {
boundElements: [{ type: "text", id: t1.id }],
});
@@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null,
});
mutateElement(r1, {
h.app.scene.mutateElement(r1, {
boundElements: [{ type: "text", id: t1.id }],
});
@@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
frameId: null,
});
mutateElement(a1, {
h.app.scene.mutateElement(a1, {
boundElements: [{ type: "text", id: t1.id }],
});
@@ -3,10 +3,7 @@ import { KEYS, updateActiveTool } from "@excalidraw/common";
import { getNonDeletedElements } from "@excalidraw/element";
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
mutateElement,
newElementWith,
} from "@excalidraw/element/mutateElement";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { getContainerElement } from "@excalidraw/element/textElement";
import {
isBoundToContainer,
@@ -94,7 +91,7 @@ const deleteSelectedElements = (
el.boundElements.forEach((candidate) => {
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
if (bound && isElbowArrow(bound)) {
mutateElement(bound, {
app.scene.mutateElement(bound, {
startBinding:
el.id === bound.startBinding?.elementId
? null
@@ -102,7 +99,6 @@ const deleteSelectedElements = (
endBinding:
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
});
mutateElement(bound, { points: bound.points });
}
});
}
@@ -261,7 +257,11 @@ export const actionDeleteSelected = register({
: endBindingElement,
};
LinearElementEditor.deletePoints(element, selectedPointsIndices);
LinearElementEditor.deletePoints(
element,
app.scene,
selectedPointsIndices,
);
return {
elements,
@@ -43,7 +43,7 @@ export const actionDuplicateSelection = register({
try {
const newAppState = LinearElementEditor.duplicateSelectedPoints(
appState,
app.scene.getNonDeletedElementsMap(),
app.scene,
);
return {
+13 -13
View File
@@ -5,7 +5,7 @@ import {
bindOrUnbindLinearElement,
} from "@excalidraw/element/binding";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { mutateElement } from "@excalidraw/element/mutateElement";
import {
isBindingElement,
isLinearElement,
@@ -46,7 +46,6 @@ export const actionFinalize = register({
element,
startBindingElement,
endBindingElement,
elementsMap,
scene,
);
}
@@ -72,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, isDragging: false },
);
}
if (window.document.activeElement instanceof HTMLElement) {
@@ -96,7 +99,7 @@ export const actionFinalize = register({
!lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint
) {
mutateElement(multiPointElement, {
scene.mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1),
});
}
@@ -120,7 +123,7 @@ export const actionFinalize = register({
if (isLoop) {
const linePoints = multiPointElement.points;
const firstPoint = linePoints[0];
mutateElement(multiPointElement, {
scene.mutateElement(multiPointElement, {
points: linePoints.map((p, index) =>
index === linePoints.length - 1
? pointFrom(firstPoint[0], firstPoint[1])
@@ -140,13 +143,7 @@ export const actionFinalize = register({
-1,
arrayToMap(elements),
);
maybeBindLinearElement(
multiPointElement,
appState,
{ x, y },
elementsMap,
elements,
);
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
}
}
@@ -202,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,
},
+4 -9
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,
}),
+5 -11
View File
@@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
},
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
const elementsMap = app.scene.getNonDeletedElementsMap();
const [x1, y1, x2, y2] = getCommonBounds(
selectedElements,
app.scene.getNonDeletedElementsMap(),
);
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
const PADDING = 16;
const frame = newFrameElement({
x: x1 - PADDING,
@@ -196,13 +194,9 @@ export const actionWrapSelectionInFrame = register({
for (const elementInGroup of elementsInGroup) {
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
mutateElement(
elementInGroup,
{
groupIds: elementInGroup.groupIds.slice(0, index),
},
false,
);
mutateElement(elementInGroup, elementsMap, {
groupIds: elementInGroup.groupIds.slice(0, index),
});
}
}
@@ -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,
@@ -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,
@@ -61,6 +58,7 @@ import type { LocalPoint } from "@excalidraw/math";
import type {
Arrowhead,
ElementsMap,
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -68,9 +66,10 @@ import type {
FontFamilyValues,
TextAlign,
VerticalAlign,
NonDeletedSceneElementsMap,
} 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";
@@ -207,25 +206,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 = (
@@ -251,10 +247,14 @@ const changeFontSize = (
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
app.scene,
);
newElement = offsetElementAfterFontResize(oldElement, newElement);
newElement = offsetElementAfterFontResize(
oldElement,
newElement,
app.scene,
);
return newElement;
}
@@ -264,15 +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 as NonDeletedSceneElementsMap,
);
updateBoundElements(element, app.scene);
}
});
@@ -778,7 +774,7 @@ type ChangeFontFamilyData = Partial<
>
> & {
/** cache of selected & editing elements populated on opened popup */
cachedElements?: Map<string, ExcalidrawElement>;
cachedElements?: ElementsMap;
/** flag to reset all elements to their cached versions */
resetAll?: true;
/** flag to reset all containers to their cached versions */
@@ -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) {
@@ -950,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(),
false,
);
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
@@ -972,8 +963,7 @@ export const actionChangeFontFamily = register({
redrawTextBoundingBox(
latestElement as ExcalidrawTextElement,
latestContainer,
app.scene.getNonDeletedElementsMap(),
false,
app.scene,
);
}
}
@@ -987,7 +977,7 @@ export const actionChangeFontFamily = register({
return result;
},
PanelComponent: ({ elements, appState, app, updateData }) => {
const cachedElementsRef = useRef<Map<string, ExcalidrawElement>>(new Map());
const cachedElementsRef = useRef<ElementsMap>(new Map());
const prevSelectedFontFamilyRef = useRef<number | null>(null);
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
@@ -996,7 +986,7 @@ export const actionChangeFontFamily = register({
const selectedFontFamily = useMemo(() => {
const getFontFamily = (
elementsArray: readonly ExcalidrawElement[],
elementsMap: Map<string, ExcalidrawElement>,
elementsMap: ElementsMap,
) =>
getFormValue(
elementsArray,
@@ -1179,7 +1169,7 @@ export const actionChangeTextAlign = register({
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
app.scene,
);
return newElement;
}
@@ -1270,7 +1260,7 @@ export const actionChangeVerticalAlign = register({
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
app.scene,
);
return newElement;
}
@@ -1670,10 +1660,10 @@ export const actionChangeArrowType = register({
newElement,
startHoveredElement,
"start",
elementsMap,
app.scene,
);
endHoveredElement &&
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
const startBinding =
startElement && newElement.startBinding
@@ -1684,7 +1674,6 @@ export const actionChangeArrowType = register({
newElement,
startElement,
"start",
elementsMap,
),
}
: null;
@@ -1697,7 +1686,6 @@ export const actionChangeArrowType = register({
newElement,
endElement,
"end",
elementsMap,
),
}
: null;
@@ -1729,7 +1717,7 @@ export const actionChangeArrowType = register({
newElement.startBinding.elementId,
) as ExcalidrawBindableElement;
if (startElement) {
bindLinearElement(newElement, startElement, "start", elementsMap);
bindLinearElement(newElement, startElement, "start", app.scene);
}
}
if (newElement.endBinding) {
@@ -1737,7 +1725,7 @@ export const actionChangeArrowType = register({
newElement.endBinding.elementId,
) as ExcalidrawBindableElement;
if (endElement) {
bindLinearElement(newElement, endElement, "end", elementsMap);
bindLinearElement(newElement, endElement, "end", app.scene);
}
}
}
@@ -1758,6 +1746,7 @@ export const actionChangeArrowType = register({
if (selected) {
newState.selectedLinearElement = new LinearElementEditor(
selected as ExcalidrawLinearElement,
arrayToMap(elements),
);
}
}
@@ -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,
+2 -5
View File
@@ -139,11 +139,8 @@ export const actionPasteStyles = register({
element.id === newElement.containerId,
) || null;
}
redrawTextBoundingBox(
newElement,
container,
app.scene.getNonDeletedElementsMap(),
);
redrawTextBoundingBox(newElement, container, app.scene);
}
if (