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, NonDeletedSceneElementsMap,
OrderedExcalidrawElement, OrderedExcalidrawElement,
Ordered, Ordered,
ElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { import type {
@ -42,7 +43,7 @@ import type {
SameType, SameType,
} from "@excalidraw/common/utility-types"; } from "@excalidraw/common/utility-types";
import type { AppState } from "../types"; import type { AppState } from "../../excalidraw/types";
type SceneStateCallback = () => void; type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void; type SceneStateCallbackRemover = () => void;
@ -166,6 +167,12 @@ class Scene {
return this.frames; return this.frames;
} }
constructor(elementsMap: ElementsMap | null = null) {
if (elementsMap) {
this.replaceAllElements(elementsMap);
}
}
getSelectedElements(opts: { getSelectedElements(opts: {
// NOTE can be ommitted by making Scene constructor require App instance // NOTE can be ommitted by making Scene constructor require App instance
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
@ -419,7 +426,6 @@ class Scene {
// TODO_SCENE: should be accessed as app.scene through the API // TODO_SCENE: should be accessed as app.scene through the API
// TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead // TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead
// 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 // 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(). // are calling it either from a React event handler or within unstable_batchedUpdates().
mutate<TElement extends Mutable<ExcalidrawElement>>( mutate<TElement extends Mutable<ExcalidrawElement>>(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import type { ElementUpdate } from "./mutateElement"; import type Scene from "./Scene";
import type { MaybeTransformHandleType } from "./transformHandles"; import type { MaybeTransformHandleType } from "./transformHandles";
import type { import type {
@ -43,12 +43,10 @@ import type {
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null, container: ExcalidrawElement | null,
elementsMap: ElementsMap, scene: Scene,
mutator: (
element: ExcalidrawElement,
updates: ElementUpdate<ExcalidrawElement>,
) => ExcalidrawElement,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
let maxWidth = undefined; let maxWidth = undefined;
const boundTextUpdates = { const boundTextUpdates = {
x: textElement.x, x: textElement.x,
@ -96,30 +94,34 @@ export const redrawTextBoundingBox = (
metrics.height, metrics.height,
container.type, container.type,
); );
mutator(container, { height: nextHeight }); scene.mutate(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
} }
if (metrics.width > maxContainerWidth) { if (metrics.width > maxContainerWidth) {
const nextWidth = computeContainerDimensionForBoundText( const nextWidth = computeContainerDimensionForBoundText(
metrics.width, metrics.width,
container.type, container.type,
); );
mutator(container, { width: nextWidth }); scene.mutate(container, { width: nextWidth });
} }
const updatedTextElement = { const updatedTextElement = {
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
} as ExcalidrawTextElementWithContainer; } as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition( const { x, y } = computeBoundTextPosition(
container, container,
updatedTextElement, updatedTextElement,
elementsMap, elementsMap,
); );
boundTextUpdates.x = x; boundTextUpdates.x = x;
boundTextUpdates.y = y; boundTextUpdates.y = y;
} }
mutator(textElement, boundTextUpdates); scene.mutate(textElement, boundTextUpdates);
}; };
export const handleBindTextResize = ( 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 { AppState } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { isFrameLikeElement } from "./typeChecks"; import { isFrameLikeElement } from "./typeChecks";
import { getElementsInGroup } from "./groups"; import { getElementsInGroup } from "./groups";
@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
import { getSelectedElements } from "./selection"; import { getSelectedElements } from "./selection";
import type Scene from "./Scene";
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types"; import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => { const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,6 @@ import {
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { import {
mutateElement, mutateElement,
mutateElementWith,
newElementWith, newElementWith,
} from "@excalidraw/element/mutateElement"; } from "@excalidraw/element/mutateElement";
import { import {
@ -38,6 +37,8 @@ import {
syncMovedIndices, syncMovedIndices,
} from "@excalidraw/element/fractionalIndex"; } from "@excalidraw/element/fractionalIndex";
import Scene from "@excalidraw/element/Scene";
import type { BindableProp, BindingProp } from "@excalidraw/element/binding"; import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
import type { ElementUpdate } from "@excalidraw/element/mutateElement"; import type { ElementUpdate } from "@excalidraw/element/mutateElement";
@ -1135,8 +1136,13 @@ export class ElementsChange implements Change<SceneElementsMap> {
} }
try { 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 // 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 // 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) // (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 // Need ordered nextElements to avoid z-index binding issues
ElementsChange.redrawBoundArrows(nextElements, changedElements); ElementsChange.redrawBoundArrows(tempScene, changedElements);
} catch (e) { } catch (e) {
console.error( console.error(
`Couldn't mutate elements after applying elements change`, `Couldn't mutate elements after applying elements change`,
@ -1459,9 +1465,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
} }
private static redrawTextBoundingBoxes( private static redrawTextBoundingBoxes(
elements: SceneElementsMap, scene: Scene,
changed: Map<string, OrderedExcalidrawElement>, changed: Map<string, OrderedExcalidrawElement>,
) { ) {
const elements = scene.getNonDeletedElementsMap();
const boxesToRedraw = new Map< const boxesToRedraw = new Map<
string, string,
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement } { container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
@ -1501,22 +1508,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
continue; continue;
} }
redrawTextBoundingBox( redrawTextBoundingBox(boundText, container, scene);
boundText,
container,
elements,
(element, updates) => mutateElementWith(element, elements, updates),
);
} }
} }
private static redrawBoundArrows( private static redrawBoundArrows(
elements: SceneElementsMap, scene: Scene,
changed: Map<string, OrderedExcalidrawElement>, changed: Map<string, OrderedExcalidrawElement>,
) { ) {
for (const element of changed.values()) { for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) { if (!element.isDeleted && isBindableElement(element)) {
updateBoundElements(element, elements, { updateBoundElements(element, scene, {
changedElements: changed, changedElements: changed,
}); });
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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