From 94c773a9909b32ac50a236c3da20169572e012f2 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Fri, 11 Apr 2025 22:07:42 +0000 Subject: [PATCH] Deprecate mutateElement, use scene.mutateElement or mutateElementWIth instead --- packages/common/src/utils.ts | 2 +- packages/element/src/align.ts | 10 +- packages/element/src/binding.ts | 103 +++++---- packages/element/src/dragElements.ts | 21 +- packages/element/src/elbowArrow.ts | 41 +--- packages/element/src/flowchart.ts | 26 ++- packages/element/src/fractionalIndex.ts | 6 +- packages/element/src/frame.ts | 32 +-- packages/element/src/linearElementEditor.ts | 114 ++++------ packages/element/src/mutateElement.ts | 71 +++++-- packages/element/src/resizeElements.ts | 106 ++++------ packages/element/src/textElement.ts | 14 +- packages/element/tests/elbowArrow.test.tsx | 8 +- packages/element/tests/resize.test.tsx | 6 +- packages/excalidraw/actions/actionAlign.tsx | 8 +- .../excalidraw/actions/actionBoundText.tsx | 52 ++--- .../actions/actionDeleteSelected.tsx | 25 +-- .../actions/actionDuplicateSelection.tsx | 20 +- .../excalidraw/actions/actionFinalize.tsx | 44 ++-- packages/excalidraw/actions/actionFlip.ts | 13 +- packages/excalidraw/actions/actionFrame.ts | 18 +- .../excalidraw/actions/actionLinearEditor.tsx | 4 +- .../excalidraw/actions/actionProperties.tsx | 84 +++++--- .../excalidraw/actions/actionSelectAll.ts | 4 +- packages/excalidraw/actions/actionStyles.ts | 1 + packages/excalidraw/change.ts | 10 +- packages/excalidraw/components/App.tsx | 197 +++++++++++------- packages/excalidraw/components/LayerUI.tsx | 28 ++- .../excalidraw/components/Stats/Angle.tsx | 15 +- .../excalidraw/components/Stats/Dimension.tsx | 9 +- .../excalidraw/components/Stats/FontSize.tsx | 1 + .../components/Stats/MultiAngle.tsx | 24 +-- .../components/Stats/MultiDimension.tsx | 28 +-- .../components/Stats/MultiFontSize.tsx | 24 +-- .../components/Stats/MultiPosition.tsx | 25 +-- .../excalidraw/components/Stats/Position.tsx | 5 - packages/excalidraw/components/Stats/utils.ts | 33 ++- packages/excalidraw/data/transform.ts | 15 +- packages/excalidraw/index.tsx | 3 +- packages/excalidraw/lasso/index.ts | 1 + packages/excalidraw/scene/Scene.ts | 29 +-- packages/excalidraw/tests/helpers/api.ts | 2 - .../tests/linearElementEditor.test.tsx | 2 +- packages/excalidraw/tests/move.test.tsx | 6 +- packages/excalidraw/wysiwyg/textWysiwyg.tsx | 7 +- 45 files changed, 593 insertions(+), 704 deletions(-) diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 7fa98eb2d..1e6bb9081 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -679,7 +679,7 @@ export const arrayToMap = ( return items.reduce((acc: Map, element) => { acc.set(typeof element === "string" ? element : element.id, element); return acc; - }, new Map()); + }, new Map() as Map); }; export const arrayToMapWithIndex = ( diff --git a/packages/element/src/align.ts b/packages/element/src/align.ts index 61535b434..eeb3b48f2 100644 --- a/packages/element/src/align.ts +++ b/packages/element/src/align.ts @@ -2,11 +2,10 @@ import type Scene from "@excalidraw/excalidraw/scene/Scene"; import { updateBoundElements } from "./binding"; import { getCommonBoundingBox } from "./bounds"; -import { mutateElement } from "./mutateElement"; import { getMaximumGroups } from "./groups"; import type { BoundingBox } from "./bounds"; -import type { ElementsMap, ExcalidrawElement } from "./types"; +import type { ExcalidrawElement } from "./types"; export interface Alignment { position: "start" | "center" | "end"; @@ -15,10 +14,10 @@ export interface Alignment { export const alignElements = ( selectedElements: ExcalidrawElement[], - elementsMap: ElementsMap, alignment: Alignment, scene: Scene, ): ExcalidrawElement[] => { + const elementsMap = scene.getNonDeletedElementsMap(); const groups: ExcalidrawElement[][] = getMaximumGroups( selectedElements, elementsMap, @@ -33,12 +32,13 @@ export const alignElements = ( ); return group.map((element) => { // update element - const updatedEle = mutateElement(element, { + const updatedEle = scene.mutate(element, { x: element.x + translation.x, y: element.y + translation.y, }); + // update bound elements - updateBoundElements(element, scene.getNonDeletedElementsMap(), { + updateBoundElements(element, elementsMap, { simultaneouslyUpdated: group, }); return updatedEle; diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 410d46624..61f1966c8 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -50,7 +50,7 @@ import { type Heading, } from "./heading"; import { LinearElementEditor } from "./linearElementEditor"; -import { mutateElement } from "./mutateElement"; +import { mutateElementWith, mutateElement } from "./mutateElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { isArrowElement, @@ -66,7 +66,7 @@ import { } from "./typeChecks"; import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; -import { mutateElbowArrow, updateElbowArrowPoints } from "./elbowArrow"; +import { updateElbowArrowPoints } from "./elbowArrow"; import type { Bounds } from "./bounds"; import type { ElementUpdate } from "./mutateElement"; @@ -84,7 +84,6 @@ import type { OrderedExcalidrawElement, ExcalidrawElbowArrowElement, FixedPoint, - SceneElementsMap, FixedPointBinding, } from "./types"; @@ -130,7 +129,6 @@ export const bindOrUnbindLinearElement = ( linearElement: NonDeleted, startBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep", - elementsMap: NonDeletedSceneElementsMap, scene: Scene, ): void => { const boundToElementIds: Set = new Set(); @@ -142,7 +140,7 @@ export const bindOrUnbindLinearElement = ( "start", boundToElementIds, unboundFromElementIds, - elementsMap, + scene, ); bindOrUnbindLinearElementEdge( linearElement, @@ -151,7 +149,7 @@ export const bindOrUnbindLinearElement = ( "end", boundToElementIds, unboundFromElementIds, - elementsMap, + scene, ); const onlyUnbound = Array.from(unboundFromElementIds).filter( @@ -159,7 +157,7 @@ export const bindOrUnbindLinearElement = ( ); getNonDeletedElements(scene, onlyUnbound).forEach((element) => { - mutateElement(element, { + scene.mutate(element, { boundElements: element.boundElements?.filter( (element) => element.type !== "arrow" || element.id !== linearElement.id, @@ -177,7 +175,7 @@ const bindOrUnbindLinearElementEdge = ( boundToElementIds: Set, // Is mutated unboundFromElementIds: Set, - elementsMap: NonDeletedSceneElementsMap, + scene: Scene, ): void => { // "keep" is for method chaining convenience, a "no-op", so just bail out if (bindableElement === "keep") { @@ -186,7 +184,7 @@ const bindOrUnbindLinearElementEdge = ( // null means break the bind, so nothing to consider here if (bindableElement === null) { - const unbound = unbindLinearElement(linearElement, startOrEnd); + const unbound = unbindLinearElement(linearElement, startOrEnd, scene); if (unbound != null) { unboundFromElementIds.add(unbound); } @@ -213,12 +211,19 @@ const bindOrUnbindLinearElementEdge = ( linearElement, bindableElement, startOrEnd, - elementsMap, + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), ); boundToElementIds.add(bindableElement.id); } } else { - bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap); + bindLinearElement( + linearElement, + bindableElement, + startOrEnd, + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), + ); boundToElementIds.add(bindableElement.id); } }; @@ -362,11 +367,9 @@ const getBindingStrategyForDraggingArrowOrJoints = ( export const bindOrUnbindLinearElements = ( selectedElements: NonDeleted[], - elementsMap: NonDeletedSceneElementsMap, - elements: readonly NonDeletedExcalidrawElement[], - scene: Scene, isBindingEnabled: boolean, draggingPoints: readonly number[] | null, + scene: Scene, zoom?: AppState["zoom"], ): void => { selectedElements.forEach((selectedElement) => { @@ -376,20 +379,20 @@ export const bindOrUnbindLinearElements = ( selectedElement, isBindingEnabled, draggingPoints ?? [], - elementsMap, - elements, + scene.getNonDeletedElementsMap(), + scene.getNonDeletedElements(), zoom, ) : // The arrow itself (the shaft) or the inner joins are dragged getBindingStrategyForDraggingArrowOrJoints( selectedElement, - elementsMap, - elements, + scene.getNonDeletedElementsMap(), + scene.getNonDeletedElements(), isBindingEnabled, zoom, ); - bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene); + bindOrUnbindLinearElement(selectedElement, start, end, scene); }); }; @@ -429,22 +432,22 @@ export const maybeBindLinearElement = ( linearElement: NonDeleted, appState: AppState, pointerCoords: { x: number; y: number }, - elementsMap: NonDeletedSceneElementsMap, - elements: readonly NonDeletedExcalidrawElement[], + scene: Scene, ): void => { if (appState.startBoundElement != null) { bindLinearElement( linearElement, appState.startBoundElement, "start", - elementsMap, + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), ); } const hoveredElement = getHoveredElementForBinding( pointerCoords, - elements, - elementsMap, + scene.getNonDeletedElements(), + scene.getNonDeletedElementsMap(), appState.zoom, isElbowArrow(linearElement), isElbowArrow(linearElement), @@ -458,7 +461,13 @@ export const maybeBindLinearElement = ( "end", ) ) { - bindLinearElement(linearElement, hoveredElement, "end", elementsMap); + bindLinearElement( + linearElement, + hoveredElement, + "end", + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), + ); } } }; @@ -487,7 +496,11 @@ export const bindLinearElement = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", - elementsMap: NonDeletedSceneElementsMap, + elementsMap: Map, + mutator: ( + element: ExcalidrawElement, + updates: ElementUpdate, + ) => ExcalidrawElement, ): void => { if (!isArrowElement(linearElement)) { return; @@ -500,7 +513,7 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, - elementsMap, + elementsMap as NonDeletedSceneElementsMap, ), hoveredElement, ), @@ -513,18 +526,17 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, - elementsMap, ), }; } - mutateElement(linearElement, { + mutator(linearElement, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, }); const boundElementsMap = arrayToMap(hoveredElement.boundElements || []); if (!boundElementsMap.has(linearElement.id)) { - mutateElement(hoveredElement, { + mutator(hoveredElement, { boundElements: (hoveredElement.boundElements || []).concat({ id: linearElement.id, type: "arrow", @@ -566,13 +578,14 @@ const isLinearElementSimple = ( const unbindLinearElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", + scene: Scene, ): ExcalidrawBindableElement["id"] | null => { const field = startOrEnd === "start" ? "startBinding" : "endBinding"; const binding = linearElement[field]; if (binding == null) { return null; } - mutateElement(linearElement, { [field]: null }); + scene.mutate(linearElement, { [field]: null }); return binding.elementId; }; @@ -740,7 +753,7 @@ const calculateFocusAndGap = ( // in explicitly. export const updateBoundElements = ( changedElement: NonDeletedExcalidrawElement, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, + elementsMap: Map, options?: { simultaneouslyUpdated?: readonly ExcalidrawElement[]; newSize?: { width: number; height: number }; @@ -796,20 +809,7 @@ export const updateBoundElements = ( // `linearElement` is being moved/scaled already, just update the binding if (simultaneouslyUpdatedElementIds.has(element.id)) { - if (isElbowArrow(element)) { - mutateElbowArrow( - element, - bindings as { - startBinding: FixedPointBinding; - endBinding: FixedPointBinding; - }, - true, - elementsMap, - ); - } else { - mutateElement(element, bindings, true); - } - + mutateElementWith(element, elementsMap, bindings); return; } @@ -898,7 +898,6 @@ export const getHeadingForElbowArrowSnap = ( otherPoint: Readonly, bindableElement: ExcalidrawBindableElement | undefined | null, aabb: Bounds | undefined | null, - elementsMap: ElementsMap, origPoint: GlobalPoint, zoom?: AppState["zoom"], ): Heading => { @@ -908,12 +907,7 @@ export const getHeadingForElbowArrowSnap = ( return otherPointHeading; } - const distance = getDistanceForBinding( - origPoint, - bindableElement, - elementsMap, - zoom, - ); + const distance = getDistanceForBinding(origPoint, bindableElement, zoom); if (!distance) { return vectorToHeading( @@ -933,7 +927,6 @@ export const getHeadingForElbowArrowSnap = ( const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, - elementsMap: ElementsMap, zoom?: AppState["zoom"], ) => { const distance = distanceToBindableElement(bindableElement, point); @@ -1239,7 +1232,6 @@ const updateBoundPoint = ( linearElement, bindableElement, startOrEnd === "startBinding" ? "start" : "end", - elementsMap, ).fixedPoint; const globalMidPoint = pointFrom( bindableElement.x + bindableElement.width / 2, @@ -1349,7 +1341,6 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", - elementsMap: ElementsMap, ): { fixedPoint: FixedPoint } => { const bounds = [ hoveredElement.x, diff --git a/packages/element/src/dragElements.ts b/packages/element/src/dragElements.ts index 669417a54..e200b375d 100644 --- a/packages/element/src/dragElements.ts +++ b/packages/element/src/dragElements.ts @@ -17,7 +17,6 @@ import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; import { updateBoundElements } from "./binding"; import { getCommonBounds } from "./bounds"; -import { mutateElement } from "./mutateElement"; import { getPerfectElementSize } from "./sizeHelpers"; import { getBoundTextElement } from "./textElement"; import { getMinTextElementWidth } from "./textMeasurements"; @@ -104,7 +103,7 @@ export const dragSelectedElements = ( ); elementsToUpdate.forEach((element) => { - updateElementCoords(pointerDownState, element, adjustedOffset); + updateElementCoords(pointerDownState, element, scene, adjustedOffset); if (!isArrowElement(element)) { // skip arrow labels since we calculate its position during render const textElement = getBoundTextElement( @@ -112,9 +111,14 @@ export const dragSelectedElements = ( scene.getNonDeletedElementsMap(), ); if (textElement) { - updateElementCoords(pointerDownState, textElement, adjustedOffset); + updateElementCoords( + pointerDownState, + textElement, + scene, + adjustedOffset, + ); } - updateBoundElements(element, scene.getElementsMapIncludingDeleted(), { + updateBoundElements(element, scene.getNonDeletedElementsMap(), { simultaneouslyUpdated: Array.from(elementsToUpdate), }); } @@ -155,6 +159,7 @@ const calculateOffset = ( const updateElementCoords = ( pointerDownState: PointerDownState, element: NonDeletedExcalidrawElement, + scene: Scene, dragOffset: { x: number; y: number }, ) => { const originalElement = @@ -163,7 +168,7 @@ const updateElementCoords = ( const nextX = originalElement.x + dragOffset.x; const nextY = originalElement.y + dragOffset.y; - mutateElement(element, { + scene.mutate(element, { x: nextX, y: nextY, }); @@ -190,6 +195,7 @@ export const dragNewElement = ({ shouldMaintainAspectRatio, shouldResizeFromCenter, zoom, + scene, widthAspectRatio = null, originOffset = null, informMutation = true, @@ -205,6 +211,7 @@ export const dragNewElement = ({ shouldMaintainAspectRatio: boolean; shouldResizeFromCenter: boolean; zoom: NormalizedZoomValue; + scene: Scene; /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is true */ widthAspectRatio?: number | null; @@ -285,7 +292,7 @@ export const dragNewElement = ({ }; } - mutateElement( + scene.mutate( newElement, { x: newX + (originOffset?.x ?? 0), @@ -295,7 +302,7 @@ export const dragNewElement = ({ ...textAutoResize, ...imageInitialDimension, }, - informMutation, + { informMutation }, ); } }; diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index e95ffc206..bf85c0189 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -22,8 +22,6 @@ import { isDevEnv, } from "@excalidraw/common"; -import type { Radians } from "@excalidraw/math"; - import type { AppState } from "@excalidraw/excalidraw/types"; import { @@ -47,12 +45,11 @@ import { vectorToHeading, headingForPoint, } from "./heading"; -import { mutateElement, type ElementUpdate } from "./mutateElement"; +import { type ElementUpdate } from "./mutateElement"; import { isBindableElement, isElbowArrow } from "./typeChecks"; import { type ExcalidrawElbowArrowElement, type NonDeletedSceneElementsMap, - type SceneElementsMap, } from "./types"; import { aabbForElement, pointInsideBounds } from "./shapes"; @@ -903,38 +900,6 @@ export const elbowArrowNeedsToGetNormalized = ( ); }; -/** - * Mutates an elbow arrow element and renormalizes it's properties if necessary. - */ -export const mutateElbowArrow = ( - element: Readonly, - updates: ElementUpdate, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap | ElementsMap, - options?: { - isDragging?: boolean; - }, -): ElementUpdate => { - invariant( - !isElbowArrow(element), - `Element "${element.type}" is not an elbow arrow! Use \`mutateElement\` instead`, - ); - - if (!elbowArrowNeedsToGetNormalized(element, updates)) { - return mutateElement(element, updates); - } - - return mutateElement(element, { - ...updates, - angle: 0 as Radians, - ...updateElbowArrowPoints( - element, - elementsMap as NonDeletedSceneElementsMap, - updates, - options, - ), - }); -}; - /** * */ @@ -1329,14 +1294,12 @@ const getElbowArrowData = ( const startHeading = getBindPointHeading( startGlobalPoint, endGlobalPoint, - elementsMap, hoveredStartElement, origStartGlobalPoint, ); const endHeading = getBindPointHeading( endGlobalPoint, startGlobalPoint, - elementsMap, hoveredEndElement, origEndGlobalPoint, ); @@ -2306,7 +2269,6 @@ const getGlobalPoint = ( const getBindPointHeading = ( p: GlobalPoint, otherPoint: GlobalPoint, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, hoveredElement: ExcalidrawBindableElement | null | undefined, origPoint: GlobalPoint, ): Heading => @@ -2324,7 +2286,6 @@ const getBindPointHeading = ( number, ], ), - elementsMap, origPoint, ); diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index 8ee2ba530..f990c1cba 100644 --- a/packages/element/src/flowchart.ts +++ b/packages/element/src/flowchart.ts @@ -7,6 +7,8 @@ import type { PendingExcalidrawElements, } from "@excalidraw/excalidraw/types"; +import type Scene from "@excalidraw/excalidraw/scene/Scene"; + import { bindLinearElement } from "./binding"; import { updateElbowArrowPoints } from "./elbowArrow"; import { @@ -239,6 +241,7 @@ const addNewNode = ( elementsMap: ElementsMap, appState: AppState, direction: LinkDirection, + scene: Scene, ) => { const successors = getSuccessors(element, elementsMap, direction); const predeccessors = getPredecessors(element, elementsMap, direction); @@ -277,6 +280,7 @@ const addNewNode = ( elementsMap, direction, appState, + scene, ); return { @@ -290,6 +294,7 @@ export const addNewNodes = ( elementsMap: ElementsMap, appState: AppState, direction: LinkDirection, + scene: Scene, numberOfNodes: number, ) => { // always start from 0 and distribute evenly @@ -355,6 +360,7 @@ export const addNewNodes = ( elementsMap, direction, appState, + scene, ); newNodes.push(nextNode); @@ -370,6 +376,7 @@ const createBindingArrow = ( elementsMap: ElementsMap, direction: LinkDirection, appState: AppState, + scene: Scene, ) => { let startX: number; let startY: number; @@ -444,13 +451,15 @@ const createBindingArrow = ( bindingArrow, startBindingElement, "start", - elementsMap as NonDeletedSceneElementsMap, + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), ); bindLinearElement( bindingArrow, endBindingElement, "end", - elementsMap as NonDeletedSceneElementsMap, + scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), ); const changedElements = new Map(); @@ -635,6 +644,7 @@ export class FlowChartCreator { elementsMap: ElementsMap, appState: AppState, direction: LinkDirection, + scene: Scene, ) { if (direction !== this.direction) { const { nextNode, bindingArrow } = addNewNode( @@ -642,6 +652,7 @@ export class FlowChartCreator { elementsMap, appState, direction, + scene, ); this.numberOfNodes = 1; @@ -655,6 +666,7 @@ export class FlowChartCreator { elementsMap, appState, direction, + scene, this.numberOfNodes, ); @@ -682,13 +694,9 @@ export class FlowChartCreator { ) ) { this.pendingNodes = this.pendingNodes.map((node) => - mutateElement( - node, - { - frameId: startNode.frameId, - }, - false, - ), + mutateElement(node, { + frameId: startNode.frameId, + }), ); } } diff --git a/packages/element/src/fractionalIndex.ts b/packages/element/src/fractionalIndex.ts index bbe739f29..378b42f3c 100644 --- a/packages/element/src/fractionalIndex.ts +++ b/packages/element/src/fractionalIndex.ts @@ -2,7 +2,7 @@ import { generateNKeysBetween } from "fractional-indexing"; import { arrayToMap } from "@excalidraw/common"; -import { mutateElement } from "./mutateElement"; +import { mutateElementWith } from "./mutateElement"; import { getBoundTextElement } from "./textElement"; import { hasBoundTextElement } from "./typeChecks"; @@ -176,7 +176,7 @@ export const syncMovedIndices = ( // split mutation so we don't end up in an incosistent state for (const [element, update] of elementsUpdates) { - mutateElement(element, update, false); + mutateElementWith(element, arrayToMap(elements), update); } } catch (e) { // fallback to default sync @@ -197,7 +197,7 @@ export const syncInvalidIndices = ( const indicesGroups = getInvalidIndicesGroups(elements); const elementsUpdates = generateIndices(elements, indicesGroups); for (const [element, update] of elementsUpdates) { - mutateElement(element, update, false); + mutateElementWith(element, arrayToMap(elements), update); } return elements as OrderedExcalidrawElement[]; diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 7f40148b7..645681ac3 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -21,7 +21,7 @@ import { getCommonBounds, getElementAbsoluteCoords, } from "./bounds"; -import { mutateElement } from "./mutateElement"; +import { mutateElementWith } from "./mutateElement"; import { getBoundTextElement, getContainerElement } from "./textElement"; import { isFrameElement, @@ -57,13 +57,9 @@ export const bindElementsToFramesAfterDuplication = ( if (nextElementId) { const nextElement = nextElementMap.get(nextElementId); if (nextElement) { - mutateElement( - nextElement, - { - frameId: nextFrameId ?? element.frameId, - }, - false, - ); + mutateElementWith(nextElement, nextElementMap, { + frameId: nextFrameId ?? element.frameId, + }); } } } @@ -567,13 +563,9 @@ export const addElementsToFrame = ( } for (const element of finalElementsToAdd) { - mutateElement( - element, - { - frameId: frame.id, - }, - false, - ); + mutateElementWith(element, elementsMap, { + frameId: frame.id, + }); } return allElements; @@ -611,13 +603,9 @@ export const removeElementsFromFrame = ( } for (const [, element] of _elementsToRemove) { - mutateElement( - element, - { - frameId: null, - }, - false, - ); + mutateElementWith(element, elementsMap, { + frameId: null, + }); } }; diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 5a0e1e1ef..f07c4262d 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -48,10 +48,8 @@ import { getMinMaxXYFromCurvePathOps, } from "./bounds"; -import { mutateElbowArrow, updateElbowArrowPoints } from "./elbowArrow"; - import { headingIsHorizontal, vectorToHeading } from "./heading"; -import { bumpVersion, mutateElement } from "./mutateElement"; +import { mutateElementWith, mutateElement } from "./mutateElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { isBindingElement, @@ -125,15 +123,17 @@ export class LinearElementEditor { public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly elbowed: boolean; - constructor(element: NonDeleted) { + constructor( + element: NonDeleted, + elementsMap: ElementsMap, + ) { this.elementId = element.id as string & { _brand: "excalidrawLinearElementId"; }; if (!pointsEqual(element.points[0], pointFrom(0, 0))) { console.error("Linear element is not normalized", Error().stack); - LinearElementEditor.normalizePoints(element); + LinearElementEditor.normalizePoints(element, elementsMap); } - this.selectedPointsIndices = null; this.lastUncommittedPoint = null; this.isDragging = false; @@ -792,11 +792,8 @@ export class LinearElementEditor { elementsMap, ); } else if (event.altKey && appState.editingLinearElement) { - if ( - linearElementEditor.lastUncommittedPoint == null && - !isElbowArrow(element) - ) { - mutateElement(element, { + if (linearElementEditor.lastUncommittedPoint == null) { + scene.mutate(element, { points: [ ...element.points, LinearElementEditor.createPointAt( @@ -862,7 +859,6 @@ export class LinearElementEditor { element, startBindingElement, endBindingElement, - elementsMap, scene, ); } @@ -1161,23 +1157,27 @@ export class LinearElementEditor { y: element.y + offsetY, }; } - // element-mutating methods // --------------------------------------------------------------------------- - - static normalizePoints(element: NonDeleted) { - mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); + static normalizePoints( + element: NonDeleted, + elementsMap: ElementsMap, + ) { + // TODO_SCENE: we don't need to inform mutation here? + mutateElementWith( + element, + elementsMap, + LinearElementEditor.getNormalizedPoints(element), + ); } - static duplicateSelectedPoints( - appState: AppState, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, - ): AppState { + static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState { invariant( appState.editingLinearElement, "Not currently editing a linear element", ); + const elementsMap = scene.getNonDeletedElementsMap(); const { selectedPointsIndices, elementId } = appState.editingLinearElement; const element = LinearElementEditor.getElement(elementId, elementsMap); @@ -1220,12 +1220,7 @@ export class LinearElementEditor { return acc; }, []); - const updates = { points: nextPoints }; - if (isElbowArrow(element)) { - mutateElbowArrow(element, updates, true, elementsMap); - } else { - mutateElement(element, updates); - } + scene.mutate(element, { points: nextPoints }); // temp hack to ensure the line doesn't move when adding point to the end, // potentially expanding the bounding box @@ -1400,8 +1395,9 @@ export class LinearElementEditor { pointerCoords: PointerCoords, app: AppClassProperties, snapToGrid: boolean, - elementsMap: ElementsMap, + scene: Scene, ) { + const elementsMap = scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement( linearElementEditor.elementId, elementsMap, @@ -1431,12 +1427,7 @@ export class LinearElementEditor { ...element.points.slice(segmentMidpoint.index!), ]; - const updates = { points }; - if (isElbowArrow(element)) { - mutateElbowArrow(element, updates, true, elementsMap); - } else { - mutateElement(element, updates); - } + scene.mutate(element, { points }); ret.pointerDownState = { ...linearElementEditor.pointerDownState, @@ -1488,28 +1479,10 @@ export class LinearElementEditor { updates.points = Array.from(nextPoints); - if (!options?.sceneElementsMap) { - mutateElbowArrow(element, updates, true, options?.sceneElementsMap!, { - isDragging: options?.isDragging, - }); - } else { - // The element is not in the scene, so we need to use the provided - // scene map. - Object.assign(element, { - ...updates, - angle: 0 as Radians, - - ...updateElbowArrowPoints( - element, - options.sceneElementsMap, - updates, - { - isDragging: options?.isDragging, - }, - ), - }); - } - bumpVersion(element); + // TODO_SCENE: fix + mutateElementWith(element, options?.sceneElementsMap!, updates, { + isDragging: options?.isDragging, + }); } else { const nextCoords = getElementPointsCoords(element, nextPoints); const prevCoords = getElementPointsCoords(element, element.points); @@ -1790,8 +1763,9 @@ export class LinearElementEditor { index: number, x: number, y: number, - elementsMap: ElementsMap, + scene: Scene, ): LinearElementEditor { + const elementsMap = scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement( linearElement.elementId, elementsMap, @@ -1834,14 +1808,9 @@ export class LinearElementEditor { .map((segment) => segment.index) .reduce((count, idx) => (idx < index ? count + 1 : count), 0); - mutateElbowArrow( - element, - { - fixedSegments: nextFixedSegments, - }, - true, - elementsMap, - ); + scene.mutate(element, { + fixedSegments: nextFixedSegments, + }); const point = pointFrom( element.x + @@ -1873,19 +1842,14 @@ export class LinearElementEditor { static deleteFixedSegment( element: ExcalidrawElbowArrowElement, - elementsMap: NonDeletedSceneElementsMap, + scene: Scene, index: number, ): void { - mutateElbowArrow( - element, - { - fixedSegments: element.fixedSegments?.filter( - (segment) => segment.index !== index, - ), - }, - true, - elementsMap, - ); + scene.mutate(element, { + fixedSegments: element.fixedSegments?.filter( + (segment) => segment.index !== index, + ), + }); } } diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 2034f8721..d089fbb66 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -2,16 +2,24 @@ import { getSizeFromPoints, randomInteger, getUpdatedTimestamp, - invariant, } from "@excalidraw/common"; +import type { Radians } from "@excalidraw/math"; + import type { Mutable } from "@excalidraw/common/utility-types"; import { ShapeCache } from "./ShapeCache"; -import { elbowArrowNeedsToGetNormalized } from "./elbowArrow"; +import { + elbowArrowNeedsToGetNormalized, + updateElbowArrowPoints, +} from "./elbowArrow"; -import type { ExcalidrawElement } from "./types"; +import type { + ExcalidrawElbowArrowElement, + ExcalidrawElement, + NonDeletedSceneElementsMap, +} from "./types"; export type ElementUpdate = Omit< Partial, @@ -20,8 +28,48 @@ export type ElementUpdate = Omit< // This function tracks updates of text elements for the purposes for collaboration. // The version is used to compare updates when more than one user is working in -// the same drawing. Note: this will trigger the component to update. Make sure you -// are calling it either from a React event handler or within unstable_batchedUpdates(). +// the same drawing. Note: this won't trigger the component to update, unlike `scene.mutate`. +export const mutateElementWith = >( + element: TElement, + elementsMap: Map, + updates: ElementUpdate, + options?: { + isDragging?: boolean; + }, +) => { + if ( + elbowArrowNeedsToGetNormalized( + element, + updates as ElementUpdate, + ) + ) { + const normalizedUpdates = { + ...updates, + angle: 0 as Radians, + ...updateElbowArrowPoints( + element as ExcalidrawElbowArrowElement, + elementsMap as NonDeletedSceneElementsMap, + updates as ElementUpdate, + options, + ), + } as ElementUpdate; + + return mutateElement( + element as ExcalidrawElbowArrowElement, + normalizedUpdates, + ); + } + + return mutateElement(element, updates); +}; + +/** + * This function tracks updates of text elements for the purposes for collaboration. + * The version is used to compare updates when more than one user is working in + * the same drawing. + * + * @deprecated Use `scene.mutate` as direct equivalent, or `mutateElementWith` in case you don't need to trigger component update. + */ export const mutateElement = >( element: TElement, updates: ElementUpdate, @@ -30,18 +78,7 @@ export const mutateElement = >( // casting to any because can't use `in` operator // (see https://github.com/microsoft/TypeScript/issues/21732) - const { points, fileId, fixedSegments, startBinding, endBinding } = - updates as any; - - invariant( - elbowArrowNeedsToGetNormalized(element, { - points, - fixedSegments, - startBinding, - endBinding, - }), - "Elbow arrow should get normalized! Use `mutateElbowArrow` instead.", - ); + const { points, fileId } = updates as any; if (typeof points !== "undefined") { updates = { ...getSizeFromPoints(points), ...updates }; diff --git a/packages/element/src/resizeElements.ts b/packages/element/src/resizeElements.ts index f7321aad8..7e457fbe2 100644 --- a/packages/element/src/resizeElements.ts +++ b/packages/element/src/resizeElements.ts @@ -32,7 +32,7 @@ import { getElementBounds, } from "./bounds"; import { LinearElementEditor } from "./linearElementEditor"; -import { mutateElement } from "./mutateElement"; +import { mutateElementWith } from "./mutateElement"; import { getBoundTextElement, getBoundTextElementId, @@ -60,8 +60,6 @@ import { import { isInGroup } from "./groups"; -import { mutateElbowArrow } from "./elbowArrow"; - import type { BoundingBox } from "./bounds"; import type { MaybeTransformHandleType, @@ -76,7 +74,6 @@ import type { ExcalidrawTextElementWithContainer, ExcalidrawImageElement, ElementsMap, - SceneElementsMap, ExcalidrawElbowArrowElement, } from "./types"; @@ -85,7 +82,6 @@ export const transformElements = ( originalElements: PointerDownState["originalElements"], transformHandleType: MaybeTransformHandleType, selectedElements: readonly NonDeletedExcalidrawElement[], - elementsMap: SceneElementsMap, scene: Scene, shouldRotateWithDiscreteAngle: boolean, shouldResizeFromCenter: boolean, @@ -95,13 +91,13 @@ export const transformElements = ( centerX: number, centerY: number, ): boolean => { + const elementsMap = scene.getNonDeletedElementsMap(); if (selectedElements.length === 1) { const [element] = selectedElements; if (transformHandleType === "rotation") { if (!isElbowArrow(element)) { rotateSingleElement( element, - elementsMap, scene, pointerX, pointerY, @@ -113,7 +109,7 @@ export const transformElements = ( resizeSingleTextElement( originalElements, element, - elementsMap, + scene, transformHandleType, shouldResizeFromCenter, pointerX, @@ -131,8 +127,6 @@ export const transformElements = ( getNextSingleWidthAndHeightFromPointer( latestElement, origElement, - elementsMap, - originalElements, transformHandleType, pointerX, pointerY, @@ -147,8 +141,8 @@ export const transformElements = ( nextHeight, latestElement, origElement, - elementsMap, originalElements, + scene, transformHandleType, { shouldMaintainAspectRatio, @@ -163,7 +157,6 @@ export const transformElements = ( rotateMultipleElements( originalElements, selectedElements, - elementsMap, scene, pointerX, pointerY, @@ -212,13 +205,15 @@ export const transformElements = ( const rotateSingleElement = ( element: NonDeletedExcalidrawElement, - elementsMap: ElementsMap, scene: Scene, pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); + const [x1, y1, x2, y2] = getElementAbsoluteCoords( + element, + scene.getNonDeletedElementsMap(), + ); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; let angle: Radians; @@ -235,13 +230,13 @@ const rotateSingleElement = ( } const boundTextElementId = getBoundTextElementId(element); - mutateElement(element, { angle }); + scene.mutate(element, { angle }); if (boundTextElementId) { const textElement = scene.getElement(boundTextElementId); if (textElement && !isArrowElement(element)) { - mutateElement(textElement, { angle }); + scene.mutate(textElement, { angle }); } } }; @@ -291,12 +286,13 @@ export const measureFontSizeFromWidth = ( const resizeSingleTextElement = ( originalElements: PointerDownState["originalElements"], element: NonDeleted, - elementsMap: ElementsMap, + scene: Scene, transformHandleType: TransformHandleDirection, shouldResizeFromCenter: boolean, pointerX: number, pointerY: number, ) => { + const elementsMap = scene.getNonDeletedElementsMap(); const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( element, elementsMap, @@ -395,7 +391,7 @@ const resizeSingleTextElement = ( ); const [nextX, nextY] = newTopLeft; - mutateElement(element, { + scene.mutate(element, { fontSize: metrics.size, width: nextWidth, height: nextHeight, @@ -510,14 +506,13 @@ const resizeSingleTextElement = ( autoResize: false, }; - mutateElement(element, resizedElement); + scene.mutate(element, resizedElement); } }; const rotateMultipleElements = ( originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], - elementsMap: SceneElementsMap, scene: Scene, pointerX: number, pointerY: number, @@ -525,6 +520,7 @@ const rotateMultipleElements = ( centerX: number, centerY: number, ) => { + const elementsMap = scene.getNonDeletedElementsMap(); let centerAngle = (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX); if (shouldRotateWithDiscreteAngle) { @@ -545,27 +541,18 @@ const rotateMultipleElements = ( (centerAngle + origAngle - element.angle) as Radians, ); - if (isElbowArrow(element)) { - // Needed to re-route the arrow - mutateElbowArrow( - element, - { + const updates = isElbowArrow(element) + ? { + // Needed to re-route the arrow points: getArrowLocalFixedPoints(element, elementsMap), - }, - false, - elementsMap, - ); - } else { - mutateElement( - element, - { + } + : { x: element.x + (rotatedCX - cx), y: element.y + (rotatedCY - cy), angle: normalizeRadians((centerAngle + origAngle) as Radians), - }, - false, - ); - } + }; + + scene.mutate(element, updates); updateBoundElements(element, elementsMap, { simultaneouslyUpdated: elements, @@ -573,15 +560,11 @@ const rotateMultipleElements = ( const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { - mutateElement( - boundText, - { - x: boundText.x + (rotatedCX - cx), - y: boundText.y + (rotatedCY - cy), - angle: normalizeRadians((centerAngle + origAngle) as Radians), - }, - false, - ); + mutateElementWith(boundText, elementsMap, { + x: boundText.x + (rotatedCX - cx), + y: boundText.y + (rotatedCY - cy), + angle: normalizeRadians((centerAngle + origAngle) as Radians), + }); } } } @@ -826,8 +809,8 @@ export const resizeSingleElement = ( nextHeight: number, latestElement: ExcalidrawElement, origElement: ExcalidrawElement, - elementsMap: ElementsMap, originalElementsMap: ElementsMap, + scene: Scene, handleDirection: TransformHandleDirection, { shouldInformMutation = true, @@ -840,6 +823,7 @@ export const resizeSingleElement = ( } = {}, ) => { let boundTextFont: { fontSize?: number } = {}; + const elementsMap = scene.getNonDeletedElementsMap(); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement) { @@ -939,7 +923,7 @@ export const resizeSingleElement = ( } if ("scale" in latestElement && "scale" in origElement) { - mutateElement(latestElement, { + scene.mutate(latestElement, { scale: [ // defaulting because scaleX/Y can be 0/-0 (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0], @@ -974,15 +958,19 @@ export const resizeSingleElement = ( ...rescaledPoints, }; - mutateElement(latestElement, updates, shouldInformMutation); + scene.mutate(latestElement, updates, { + informMutation: shouldInformMutation, + }); - updateBoundElements(latestElement, elementsMap as SceneElementsMap, { + const elementsMap = scene.getNonDeletedElementsMap(); + + updateBoundElements(latestElement, elementsMap, { // TODO: confirm with MARK if this actually makes sense newSize: { width: nextWidth, height: nextHeight }, }); if (boundTextElement && boundTextFont != null) { - mutateElement(boundTextElement, { + scene.mutate(boundTextElement, { fontSize: boundTextFont.fontSize, }); } @@ -998,8 +986,6 @@ export const resizeSingleElement = ( const getNextSingleWidthAndHeightFromPointer = ( latestElement: ExcalidrawElement, origElement: ExcalidrawElement, - elementsMap: ElementsMap, - originalElementsMap: ElementsMap, handleDirection: TransformHandleDirection, pointerX: number, pointerY: number, @@ -1534,26 +1520,22 @@ export const resizeMultipleElements = ( } of elementsAndUpdates) { const { width, height, angle } = update; - scene.mutateElement(element, update, false, { + scene.mutate(element, update, { // needed for the fixed binding point udpate to take effect isDragging: true, }); - updateBoundElements(element, elementsMap as SceneElementsMap, { + updateBoundElements(element, scene.getNonDeletedElementsMap(), { simultaneouslyUpdated: elementsToUpdate, newSize: { width, height }, }); const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && boundTextFontSize) { - scene.mutateElement( - boundTextElement, - { - fontSize: boundTextFontSize, - angle: isLinearElement(element) ? undefined : angle, - }, - false, - ); + scene.mutate(boundTextElement, { + fontSize: boundTextFontSize, + angle: isLinearElement(element) ? undefined : angle, + }); handleBindTextResize(element, elementsMap, handleDirection, true); } } diff --git a/packages/element/src/textElement.ts b/packages/element/src/textElement.ts index ea27c318f..0514cb2c6 100644 --- a/packages/element/src/textElement.ts +++ b/packages/element/src/textElement.ts @@ -17,6 +17,7 @@ import { updateOriginalContainerCache, } from "./containerCache"; import { LinearElementEditor } from "./linearElementEditor"; + import { mutateElement } from "./mutateElement"; import { measureText } from "./textMeasurements"; import { wrapText } from "./textWrapping"; @@ -26,6 +27,8 @@ import { isTextElement, } from "./typeChecks"; +import type { ElementUpdate } from "./mutateElement"; + import type { MaybeTransformHandleType } from "./transformHandles"; import type { ElementsMap, @@ -41,7 +44,10 @@ export const redrawTextBoundingBox = ( textElement: ExcalidrawTextElement, container: ExcalidrawElement | null, elementsMap: ElementsMap, - informMutation = true, + mutator: ( + element: ExcalidrawElement, + updates: ElementUpdate, + ) => ExcalidrawElement, ) => { let maxWidth = undefined; const boundTextUpdates = { @@ -90,7 +96,7 @@ export const redrawTextBoundingBox = ( metrics.height, container.type, ); - mutateElement(container, { height: nextHeight }, informMutation); + mutator(container, { height: nextHeight }); updateOriginalContainerCache(container.id, nextHeight); } if (metrics.width > maxContainerWidth) { @@ -98,7 +104,7 @@ export const redrawTextBoundingBox = ( metrics.width, container.type, ); - mutateElement(container, { width: nextWidth }, informMutation); + mutator(container, { width: nextWidth }); } const updatedTextElement = { ...textElement, @@ -113,7 +119,7 @@ export const redrawTextBoundingBox = ( boundTextUpdates.y = y; } - mutateElement(textElement, boundTextUpdates, informMutation); + mutator(textElement, boundTextUpdates); }; export const handleBindTextResize = ( diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index b8b5a8b85..824e3e666 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -188,8 +188,12 @@ describe("elbow arrow routing", () => { scene.insertElement(rectangle2); scene.insertElement(arrow); const elementsMap = scene.getNonDeletedElementsMap(); - bindLinearElement(arrow, rectangle1, "start", elementsMap); - bindLinearElement(arrow, rectangle2, "end", elementsMap); + bindLinearElement(arrow, rectangle1, "start", elementsMap, (...args) => + scene.mutate(...args), + ); + bindLinearElement(arrow, rectangle2, "end", elementsMap, (...args) => + scene.mutate(...args), + ); expect(arrow.startBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null); diff --git a/packages/element/tests/resize.test.tsx b/packages/element/tests/resize.test.tsx index f3804e2a2..98fbf2a9a 100644 --- a/packages/element/tests/resize.test.tsx +++ b/packages/element/tests/resize.test.tsx @@ -333,7 +333,7 @@ describe("line element", () => { element, element, h.app.scene.getNonDeletedElementsMap(), - h.app.scene.getNonDeletedElementsMap(), + h.app.scene, "ne", ); @@ -369,7 +369,7 @@ describe("line element", () => { element, element, h.app.scene.getNonDeletedElementsMap(), - h.app.scene.getNonDeletedElementsMap(), + h.app.scene, "se", ); @@ -424,7 +424,7 @@ describe("line element", () => { element, element, h.app.scene.getNonDeletedElementsMap(), - h.app.scene.getNonDeletedElementsMap(), + h.app.scene, "e", { shouldResizeFromCenter: true, diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx index 46023d61e..0ef938c67 100644 --- a/packages/excalidraw/actions/actionAlign.tsx +++ b/packages/excalidraw/actions/actionAlign.tsx @@ -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); diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index bada6fc44..90680545f 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -21,27 +21,22 @@ import { import { hasBoundTextElement, - isElbowArrow, isTextBindableContainer, isTextElement, isUsingAdaptiveRadius, } from "@excalidraw/element/typeChecks"; -import { mutateElement } from "@excalidraw/element/mutateElement"; import { measureText } from "@excalidraw/element/textMeasurements"; import { syncMovedIndices } from "@excalidraw/element/fractionalIndex"; import { newElement } from "@excalidraw/element/newElement"; -import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; - import type { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextContainer, ExcalidrawTextElement, - FixedPointBinding, } from "@excalidraw/element/types"; import type { Mutable } from "@excalidraw/common/utility-types"; @@ -81,7 +76,7 @@ export const actionUnbindText = register({ boundTextElement, elementsMap, ); - mutateElement(boundTextElement as ExcalidrawTextElement, { + app.scene.mutate(boundTextElement as ExcalidrawTextElement, { containerId: null, width, height, @@ -89,7 +84,7 @@ export const actionUnbindText = register({ x, y, }); - mutateElement(element, { + app.scene.mutate(element, { boundElements: element.boundElements?.filter( (ele) => ele.id !== boundTextElement.id, ), @@ -154,13 +149,13 @@ export const actionBindText = register({ textElement = selectedElements[1] as ExcalidrawTextElement; container = selectedElements[0] as ExcalidrawTextContainer; } - mutateElement(textElement, { + app.scene.mutate(textElement, { containerId: container.id, verticalAlign: VERTICAL_ALIGN.MIDDLE, textAlign: TEXT_ALIGN.CENTER, autoResize: true, }); - mutateElement(container, { + app.scene.mutate(container, { boundElements: (container.boundElements || []).concat({ type: "text", id: textElement.id, @@ -171,6 +166,7 @@ export const actionBindText = register({ textElement, container, app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); // overwritting the cache with original container height so // it can be restored when unbind @@ -301,40 +297,26 @@ export const actionWrapTextInContainer = register({ } if (startBinding || endBinding) { - const updates = { startBinding, endBinding }; - - if (isElbowArrow(ele)) { - mutateElbowArrow( - ele, - updates as { - startBinding: FixedPointBinding; - endBinding: FixedPointBinding; - }, - false, - app.scene.getNonDeletedElementsMap(), - ); - } else { - mutateElement(ele, updates, false); - } + app.scene.mutate(ele, { + startBinding, + endBinding, + }); } }); } - mutateElement( - textElement, - { - containerId: container.id, - verticalAlign: VERTICAL_ALIGN.MIDDLE, - boundElements: null, - textAlign: TEXT_ALIGN.CENTER, - autoResize: true, - }, - false, - ); + app.scene.mutate(textElement, { + containerId: container.id, + verticalAlign: VERTICAL_ALIGN.MIDDLE, + boundElements: null, + textAlign: TEXT_ALIGN.CENTER, + autoResize: true, + }); redrawTextBoundingBox( textElement, container, app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); updatedElements = pushContainerBelowText( diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx index d861390b3..ab2ae05b0 100644 --- a/packages/excalidraw/actions/actionDeleteSelected.tsx +++ b/packages/excalidraw/actions/actionDeleteSelected.tsx @@ -17,8 +17,6 @@ import { selectGroupsForSelectedElements, } from "@excalidraw/element/groups"; -import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; - import type { ExcalidrawElement } from "@excalidraw/element/types"; import { t } from "../i18n"; @@ -93,21 +91,14 @@ const deleteSelectedElements = ( el.boundElements.forEach((candidate) => { const bound = app.scene.getNonDeletedElementsMap().get(candidate.id); if (bound && isElbowArrow(bound)) { - mutateElbowArrow( - bound, - { - startBinding: - el.id === bound.startBinding?.elementId - ? null - : bound.startBinding, - endBinding: - el.id === bound.endBinding?.elementId - ? null - : bound.endBinding, - }, - true, - app.scene.getNonDeletedElementsMap(), - ); + app.scene.mutate(bound, { + startBinding: + el.id === bound.startBinding?.elementId + ? null + : bound.startBinding, + endBinding: + el.id === bound.endBinding?.elementId ? null : bound.endBinding, + }); } }); } diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index 231fe1913..9362961ef 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -25,7 +25,7 @@ import { syncMovedIndices } from "@excalidraw/element/fractionalIndex"; import { duplicateElements } from "@excalidraw/element/duplicate"; -import type { ExcalidrawElement } from "@excalidraw/element/types"; +import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import { ToolButton } from "../components/ToolButton"; import { DuplicateIcon } from "../components/icons"; @@ -52,7 +52,7 @@ export const actionDuplicateSelection = register({ try { const newAppState = LinearElementEditor.duplicateSelectedPoints( appState, - app.scene.getNonDeletedElementsMap(), + app.scene, ); return { @@ -91,11 +91,16 @@ export const actionDuplicateSelection = register({ } } + syncMovedIndices(nextElements, arrayToMap(duplicatedElements)); + return { - elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)), + elements: nextElements, appState: { ...appState, - ...updateLinearElementEditors(duplicatedElements), + ...updateLinearElementEditors( + duplicatedElements, + arrayToMap(nextElements), + ), ...selectGroupsForSelectedElements( { editingGroupId: appState.editingGroupId, @@ -131,7 +136,10 @@ export const actionDuplicateSelection = register({ ), }); -const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => { +const updateLinearElementEditors = ( + clonedElements: ExcalidrawElement[], + elementsMap: ElementsMap, +) => { const linears = clonedElements.filter(isLinearElement); if (linears.length === 1) { const linear = linears[0]; @@ -142,7 +150,7 @@ const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => { if (onlySingleLinearSelected) { return { - selectedLinearElement: new LinearElementEditor(linear), + selectedLinearElement: new LinearElementEditor(linear, elementsMap), }; } } diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f78592326..45f1c3a53 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -6,11 +6,8 @@ import { } from "@excalidraw/element/binding"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; -import { mutateElement } from "@excalidraw/element/mutateElement"; -import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; import { isBindingElement, - isElbowArrow, isLinearElement, } from "@excalidraw/element/typeChecks"; @@ -19,13 +16,6 @@ import { isPathALoop } from "@excalidraw/element/shapes"; import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers"; -import type { - ExcalidrawElbowArrowElement, - ExcalidrawElement, -} from "@excalidraw/element/types"; - -import type { ElementUpdate } from "@excalidraw/element/mutateElement"; - import { t } from "../i18n"; import { resetCursor } from "../cursor"; import { done } from "../components/icons"; @@ -56,7 +46,6 @@ export const actionFinalize = register({ element, startBindingElement, endBindingElement, - elementsMap, scene, ); } @@ -82,7 +71,11 @@ export const actionFinalize = register({ scene.getElement(appState.pendingImageElementId); if (pendingImageElement) { - mutateElement(pendingImageElement, { isDeleted: true }, false); + scene.mutate( + pendingImageElement, + { isDeleted: true }, + { informMutation: false }, + ); } if (window.document.activeElement instanceof HTMLElement) { @@ -95,16 +88,6 @@ export const actionFinalize = register({ ? appState.newElement : null; - const mutate = (updates: ElementUpdate) => - isElbowArrow(multiPointElement as ExcalidrawElbowArrowElement) - ? mutateElbowArrow( - multiPointElement as ExcalidrawElbowArrowElement, - updates, - true, - elementsMap, - ) - : mutateElement(multiPointElement as ExcalidrawElement, updates); - if (multiPointElement) { // pen and mouse have hover if ( @@ -116,7 +99,7 @@ export const actionFinalize = register({ !lastCommittedPoint || points[points.length - 1] !== lastCommittedPoint ) { - mutate({ + scene.mutate(multiPointElement, { points: multiPointElement.points.slice(0, -1), }); } @@ -140,7 +123,7 @@ export const actionFinalize = register({ if (isLoop) { const linePoints = multiPointElement.points; const firstPoint = linePoints[0]; - mutate({ + scene.mutate(multiPointElement, { points: linePoints.map((p, index) => index === linePoints.length - 1 ? pointFrom(firstPoint[0], firstPoint[1]) @@ -160,13 +143,7 @@ export const actionFinalize = register({ -1, arrayToMap(elements), ); - maybeBindLinearElement( - multiPointElement, - appState, - { x, y }, - elementsMap, - elements, - ); + maybeBindLinearElement(multiPointElement, appState, { x, y }, scene); } } @@ -222,7 +199,10 @@ export const actionFinalize = register({ // To select the linear element when user has finished mutipoint editing selectedLinearElement: multiPointElement && isLinearElement(multiPointElement) - ? new LinearElementEditor(multiPointElement) + ? new LinearElementEditor( + multiPointElement, + arrayToMap(newElements), + ) : appState.selectedLinearElement, pendingImageElementId: null, }, diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index 62be891cb..1b8b2661d 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -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.mutate(element, { x: element.x + diffX, y: element.y + diffY, }), ); elbowArrows.forEach((element) => - mutateElement(element, { + app.scene.mutate(element, { x: element.x + diffX, y: element.y + diffY, }), diff --git a/packages/excalidraw/actions/actionFrame.ts b/packages/excalidraw/actions/actionFrame.ts index 13d57b2a1..7db4dad71 100644 --- a/packages/excalidraw/actions/actionFrame.ts +++ b/packages/excalidraw/actions/actionFrame.ts @@ -1,5 +1,5 @@ import { getNonDeletedElements } from "@excalidraw/element"; -import { mutateElement } from "@excalidraw/element/mutateElement"; +import { mutateElementWith } from "@excalidraw/element/mutateElement"; import { newFrameElement } from "@excalidraw/element/newElement"; import { isFrameLikeElement } from "@excalidraw/element/typeChecks"; import { @@ -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, - ); + mutateElementWith(elementInGroup, elementsMap, { + groupIds: elementInGroup.groupIds.slice(0, index), + }); } } diff --git a/packages/excalidraw/actions/actionLinearEditor.tsx b/packages/excalidraw/actions/actionLinearEditor.tsx index 56e327bd2..1645554bf 100644 --- a/packages/excalidraw/actions/actionLinearEditor.tsx +++ b/packages/excalidraw/actions/actionLinearEditor.tsx @@ -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, diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 5a309b677..b3b6a3bbe 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -34,10 +34,7 @@ import { import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; -import { - mutateElement, - newElementWith, -} from "@excalidraw/element/mutateElement"; +import { newElementWith } from "@excalidraw/element/mutateElement"; import { getBoundTextElement, @@ -68,7 +65,6 @@ import type { FontFamilyValues, TextAlign, VerticalAlign, - NonDeletedSceneElementsMap, } from "@excalidraw/element/types"; import { trackEvent } from "../analytics"; @@ -138,6 +134,7 @@ import { register } from "./register"; import type { CaptureUpdateActionType } from "../store"; import type { AppClassProperties, AppState, Primitive } from "../types"; +import type Scene from "../scene/Scene"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; @@ -207,25 +204,22 @@ export const getFormValue = function ( 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.mutate(nextElement, { + x: + prevElement.textAlign === "left" + ? prevElement.x + : prevElement.x + + (prevElement.width - nextElement.width) / + (prevElement.textAlign === "center" ? 2 : 1), + // centering vertically is non-standard, but for Excalidraw I think + // it makes sense + y: prevElement.y + (prevElement.height - nextElement.height) / 2, + }); }; const changeFontSize = ( @@ -252,9 +246,14 @@ const changeFontSize = ( newElement, app.scene.getContainerElement(oldElement), app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); - newElement = offsetElementAfterFontResize(oldElement, newElement); + newElement = offsetElementAfterFontResize( + oldElement, + newElement, + app.scene, + ); return newElement; } @@ -269,10 +268,7 @@ const changeFontSize = ( includeBoundTextElement: true, }).forEach((element) => { if (isTextElement(element)) { - updateBoundElements( - element, - updatedElementsMap as NonDeletedSceneElementsMap, - ); + updateBoundElements(element, updatedElementsMap); } }); @@ -919,7 +915,7 @@ export const actionChangeFontFamily = register({ if (resetContainers && container && cachedContainer) { // reset the container back to it's cached version - mutateElement(container, { ...cachedContainer }, false); + app.scene.mutate(container, { ...cachedContainer }); } if (!skipFontFaceCheck) { @@ -954,7 +950,7 @@ export const actionChangeFontFamily = register({ element, container, app.scene.getNonDeletedElementsMap(), - false, + (...args) => app.scene.mutate(...args), ); } } else { @@ -973,7 +969,7 @@ export const actionChangeFontFamily = register({ latestElement as ExcalidrawTextElement, latestContainer, app.scene.getNonDeletedElementsMap(), - false, + (...args) => app.scene.mutate(...args), ); } } @@ -1180,6 +1176,7 @@ export const actionChangeTextAlign = register({ newElement, app.scene.getContainerElement(oldElement), app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); return newElement; } @@ -1271,6 +1268,7 @@ export const actionChangeVerticalAlign = register({ newElement, app.scene.getContainerElement(oldElement), app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); return newElement; } @@ -1670,10 +1668,17 @@ export const actionChangeArrowType = register({ newElement, startHoveredElement, "start", - elementsMap, + app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); endHoveredElement && - bindLinearElement(newElement, endHoveredElement, "end", elementsMap); + bindLinearElement( + newElement, + endHoveredElement, + "end", + app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), + ); const startBinding = startElement && newElement.startBinding @@ -1684,7 +1689,6 @@ export const actionChangeArrowType = register({ newElement, startElement, "start", - elementsMap, ), } : null; @@ -1697,7 +1701,6 @@ export const actionChangeArrowType = register({ newElement, endElement, "end", - elementsMap, ), } : null; @@ -1729,7 +1732,13 @@ export const actionChangeArrowType = register({ newElement.startBinding.elementId, ) as ExcalidrawBindableElement; if (startElement) { - bindLinearElement(newElement, startElement, "start", elementsMap); + bindLinearElement( + newElement, + startElement, + "start", + app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), + ); } } if (newElement.endBinding) { @@ -1737,7 +1746,13 @@ export const actionChangeArrowType = register({ newElement.endBinding.elementId, ) as ExcalidrawBindableElement; if (endElement) { - bindLinearElement(newElement, endElement, "end", elementsMap); + bindLinearElement( + newElement, + endElement, + "end", + app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), + ); } } } @@ -1758,6 +1773,7 @@ export const actionChangeArrowType = register({ if (selected) { newState.selectedLinearElement = new LinearElementEditor( selected as ExcalidrawLinearElement, + arrayToMap(elements), ); } } diff --git a/packages/excalidraw/actions/actionSelectAll.ts b/packages/excalidraw/actions/actionSelectAll.ts index d7775774a..ea13636b7 100644 --- a/packages/excalidraw/actions/actionSelectAll.ts +++ b/packages/excalidraw/actions/actionSelectAll.ts @@ -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, diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts index ed3c91e30..b2a248a6c 100644 --- a/packages/excalidraw/actions/actionStyles.ts +++ b/packages/excalidraw/actions/actionStyles.ts @@ -143,6 +143,7 @@ export const actionPasteStyles = register({ newElement, container, app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); } diff --git a/packages/excalidraw/change.ts b/packages/excalidraw/change.ts index 28eaf994f..9b83cfc49 100644 --- a/packages/excalidraw/change.ts +++ b/packages/excalidraw/change.ts @@ -16,6 +16,7 @@ import { import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; import { mutateElement, + mutateElementWith, newElementWith, } from "@excalidraw/element/mutateElement"; import { @@ -490,6 +491,7 @@ export class AppStateChange implements Change { nextElements.get( selectedLinearElementId, ) as NonDeleted, + nextElements, ) : null; @@ -499,6 +501,7 @@ export class AppStateChange implements Change { nextElements.get( editingLinearElementId, ) as NonDeleted, + nextElements, ) : null; @@ -1498,7 +1501,12 @@ export class ElementsChange implements Change { continue; } - redrawTextBoundingBox(boundText, container, elements, false); + redrawTextBoundingBox( + boundText, + container, + elements, + (element, updates) => mutateElementWith(element, elements, updates), + ); } } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 0619f6c16..1a3c932dc 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1402,7 +1402,7 @@ class App extends React.Component { private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => { if (frame) { - this.scene.mutateElement(frame, { name: frame.name?.trim() || null }); + this.scene.mutate(frame, { name: frame.name?.trim() || null }); } this.setState({ editingFrame: null }); }; @@ -1459,7 +1459,7 @@ class App extends React.Component { autoFocus value={frameNameInEdit} onChange={(e) => { - this.scene.mutateElement(f, { + this.scene.mutate(f, { name: e.target.value, }); }} @@ -1950,13 +1950,21 @@ class App extends React.Component { // state only. // Thus reset so that we prefer local cache (if there was some // generationData set previously) - this.scene.mutateElement(frameElement, { - customData: { generationData: undefined }, - }, { informMutation: false }); + this.scene.mutate( + frameElement, + { + customData: { generationData: undefined }, + }, + { informMutation: false }, + ); } else { - this.scene.mutateElement(frameElement, { - customData: { generationData: data }, - }, { informMutation: false }); + this.scene.mutate( + frameElement, + { + customData: { generationData: data }, + }, + { informMutation: false }, + ); } this.magicGenerations.set(frameElement.id, data); this.triggerRender(); @@ -2127,7 +2135,7 @@ class App extends React.Component { this.scene.insertElement(frame); for (const child of selectedElements) { - this.scene.mutateElement(child, { frameId: frame.id }); + this.scene.mutate(child, { frameId: frame.id }); } this.setState({ @@ -2926,8 +2934,7 @@ class App extends React.Component { nonDeletedElementsMap, ), ), - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), + this.scene, ); } @@ -3329,6 +3336,7 @@ class App extends React.Component { newElement, container, this.scene.getElementsMapIncludingDeleted(), + (...args) => this.scene.mutate(...args), ); } }); @@ -3452,7 +3460,11 @@ class App extends React.Component { } // hack to reset the `y` coord because we vertically center during // insertImageElement - this.scene.mutateElement(initializedImageElement, { y }, { informMutation: false }); + this.scene.mutate( + initializedImageElement, + { y }, + { informMutation: false }, + ); y = imageElement.y + imageElement.height + 25; @@ -4177,6 +4189,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), this.state, getLinkDirectionFromKey(event.key), + this.scene, ); } @@ -4418,10 +4431,14 @@ class App extends React.Component { } selectedElements.forEach((element) => { - this.scene.mutateElement(element, { - x: element.x + offsetX, - y: element.y + offsetY, - }, { informMutation: false }); + this.scene.mutate( + element, + { + x: element.x + offsetX, + y: element.y + offsetY, + }, + { informMutation: false }, + ); updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { simultaneouslyUpdated: selectedElements, @@ -4457,6 +4474,7 @@ class App extends React.Component { this.setState({ editingLinearElement: new LinearElementEditor( selectedElement, + this.scene.getNonDeletedElementsMap(), ), }); } @@ -4650,11 +4668,9 @@ class App extends React.Component { if (isArrowKey(event.key)) { bindOrUnbindLinearElements( this.scene.getSelectedElements(this.state).filter(isLinearElement), - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), - this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? [], + this.scene, this.state.zoom, ); this.setState({ suggestedBindings: [] }); @@ -5324,7 +5340,7 @@ class App extends React.Component { const minHeight = getApproxMinLineHeight(fontSize, lineHeight); const newHeight = Math.max(container.height, minHeight); const newWidth = Math.max(container.width, minWidth); - this.scene.mutateElement(container, { + this.scene.mutate(container, { height: newHeight, width: newWidth, }); @@ -5378,7 +5394,7 @@ class App extends React.Component { }); if (!existingTextElement && shouldBindToContainer && container) { - this.scene.mutateElement(container, { + this.scene.mutate(container, { boundElements: (container.boundElements || []).concat({ type: "text", id: element.id, @@ -5454,7 +5470,10 @@ class App extends React.Component { ) { this.store.shouldCaptureIncrement(); this.setState({ - editingLinearElement: new LinearElementEditor(selectedElements[0]), + editingLinearElement: new LinearElementEditor( + selectedElements[0], + this.scene.getNonDeletedElementsMap(), + ), }); return; } else if ( @@ -5480,7 +5499,7 @@ class App extends React.Component { this.store.shouldCaptureIncrement(); LinearElementEditor.deleteFixedSegment( selectedElements[0], - this.scene.getNonDeletedElementsMap(), + this.scene, midPoint, ); @@ -5927,7 +5946,7 @@ class App extends React.Component { lastPoint, ) >= LINE_CONFIRM_THRESHOLD ) { - this.scene.mutateElement( + this.scene.mutate( multiElement, { points: [ @@ -5951,7 +5970,7 @@ class App extends React.Component { ) < LINE_CONFIRM_THRESHOLD ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - this.scene.mutateElement( + this.scene.mutate( multiElement, { points: points.slice(0, -1), @@ -5990,7 +6009,7 @@ class App extends React.Component { } // update last uncommitted point - this.scene.mutateElement( + this.scene.mutate( multiElement, { points: [ @@ -6675,7 +6694,7 @@ class App extends React.Component { const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); - this.scene.mutateElement(pendingImageElement, { + this.scene.mutate(pendingImageElement, { x, y, frameId: frame ? frame.id : null, @@ -7729,7 +7748,7 @@ class App extends React.Component { multiElement.type === "line" && isPathALoop(multiElement.points, this.state.zoom.value) ) { - this.scene.mutateElement(multiElement, { + this.scene.mutate(multiElement, { lastCommittedPoint: multiElement.points[multiElement.points.length - 1], }); @@ -7740,7 +7759,7 @@ class App extends React.Component { // Elbow arrows cannot be created by putting down points // only the start and end points can be defined if (isElbowArrow(multiElement) && multiElement.points.length > 1) { - this.scene.mutateElement(multiElement, { + this.scene.mutate(multiElement, { lastCommittedPoint: multiElement.points[multiElement.points.length - 1], }); @@ -7777,7 +7796,7 @@ class App extends React.Component { })); // clicking outside commit zone → update reference for last committed // point - this.scene.mutateElement(multiElement, { + this.scene.mutate(multiElement, { lastCommittedPoint: multiElement.points[multiElement.points.length - 1], }); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); @@ -7863,7 +7882,7 @@ class App extends React.Component { ), }; }); - this.scene.mutateElement(element, { + this.scene.mutate(element, { points: [...element.points, pointFrom(0, 0)], }); const boundElement = getHoveredElementForBinding( @@ -8113,7 +8132,7 @@ class App extends React.Component { index, gridX, gridY, - this.scene.getNonDeletedElementsMap(), + this.scene, ); flushSync(() => { @@ -8218,7 +8237,7 @@ class App extends React.Component { pointerCoords, this, !event[KEYS.CTRL_OR_CMD], - elementsMap, + this.scene, ); if (!ret) { return; @@ -8445,7 +8464,7 @@ class App extends React.Component { ), }; - this.scene.mutateElement(croppingElement, { + this.scene.mutate(croppingElement, { crop: nextCrop, }); @@ -8642,7 +8661,7 @@ class App extends React.Component { ? newElement.pressures : [...newElement.pressures, event.pressure]; - this.scene.mutateElement( + this.scene.mutate( newElement, { points: [...points, pointFrom(dx, dy)], @@ -8673,7 +8692,7 @@ class App extends React.Component { } if (points.length === 1) { - this.scene.mutateElement( + this.scene.mutate( newElement, { points: [...points, pointFrom(dx, dy)], @@ -8684,7 +8703,7 @@ class App extends React.Component { points.length === 2 || (points.length > 1 && isElbowArrow(newElement)) ) { - this.scene.mutateElement( + this.scene.mutate( newElement, { points: [...points.slice(0, -1), pointFrom(dx, dy)], @@ -8800,7 +8819,10 @@ class App extends React.Component { selectedLinearElement: elementsWithinSelection.length === 1 && isLinearElement(elementsWithinSelection[0]) - ? new LinearElementEditor(elementsWithinSelection[0]) + ? new LinearElementEditor( + elementsWithinSelection[0], + this.scene.getNonDeletedElementsMap(), + ) : null, showHyperlinkPopup: elementsWithinSelection.length === 1 && @@ -8900,7 +8922,7 @@ class App extends React.Component { .map((e) => elementsMap.get(e.id)) .filter((e) => isElbowArrow(e)) .forEach((e) => { - !!e && this.scene.mutateElement(e, {}); + !!e && this.scene.mutate(e, {}); }); } } @@ -8936,10 +8958,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); if (element) { - this.scene.mutateElement( - element as ExcalidrawElbowArrowElement, - {}, - ); + this.scene.mutate(element as ExcalidrawElbowArrowElement, {}); } } @@ -8968,7 +8987,6 @@ class App extends React.Component { element, startBindingElement, endBindingElement, - elementsMap, this.scene, ); } @@ -9035,7 +9053,7 @@ class App extends React.Component { ? [] : [...newElement.pressures, childEvent.pressure]; - this.scene.mutateElement(newElement, { + this.scene.mutate(newElement, { points: [...points, pointFrom(dx, dy)], pressures, lastCommittedPoint: pointFrom(dx, dy), @@ -9082,7 +9100,7 @@ class App extends React.Component { ); if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { - this.scene.mutateElement( + this.scene.mutate( newElement, { points: [ @@ -9109,8 +9127,7 @@ class App extends React.Component { newElement, this.state, pointerCoords, - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), + this.scene, ); } this.setState({ suggestedBindings: [], startBoundElement: null }); @@ -9128,7 +9145,10 @@ class App extends React.Component { }, prevState, ), - selectedLinearElement: new LinearElementEditor(newElement), + selectedLinearElement: new LinearElementEditor( + newElement, + this.scene.getNonDeletedElementsMap(), + ), })); } else { this.setState((prevState) => ({ @@ -9151,7 +9171,7 @@ class App extends React.Component { ); if (newElement.width < minWidth) { - this.scene.mutateElement(newElement, { + this.scene.mutate(newElement, { autoResize: true, }); } @@ -9201,13 +9221,9 @@ class App extends React.Component { } if (newElement) { - this.scene.mutateElement( - newElement, - getNormalizedDimensions(newElement), - { - informMutation: false, - }, - ); + this.scene.mutate(newElement, getNormalizedDimensions(newElement), { + informMutation: false, + }); // the above does not guarantee the scene to be rendered again, hence the trigger below this.scene.triggerUpdate(); } @@ -9239,7 +9255,7 @@ class App extends React.Component { ) { // remove the linear element from all groups // before removing it from the frame as well - this.scene.mutateElement(linearElement, { + this.scene.mutate(linearElement, { groupIds: [], }); @@ -9268,9 +9284,13 @@ class App extends React.Component { this.state.editingGroupId!, ); - this.scene.mutateElement(element, { - groupIds: element.groupIds.slice(0, index), - }, { informMutation: false }); + this.scene.mutate( + element, + { + groupIds: element.groupIds.slice(0, index), + }, + { informMutation: false }, + ); } nextElements.forEach((element) => { @@ -9281,9 +9301,13 @@ class App extends React.Component { element.groupIds[element.groupIds.length - 1], ).length < 2 ) { - this.scene.mutateElement(element, { - groupIds: [], - }, { informMutation: false }); + this.scene.mutate( + element, + { + groupIds: [], + }, + { informMutation: false }, + ); } }); @@ -9392,7 +9416,10 @@ class App extends React.Component { // the one we've hit if (selectedElements.length === 1) { this.setState({ - selectedLinearElement: new LinearElementEditor(hitElement), + selectedLinearElement: new LinearElementEditor( + hitElement, + this.scene.getNonDeletedElementsMap(), + ), }); } } @@ -9517,7 +9544,10 @@ class App extends React.Component { selectedLinearElement: newSelectedElements.length === 1 && isLinearElement(newSelectedElements[0]) - ? new LinearElementEditor(newSelectedElements[0]) + ? new LinearElementEditor( + newSelectedElements[0], + this.scene.getNonDeletedElementsMap(), + ) : prevState.selectedLinearElement, }; }); @@ -9591,7 +9621,10 @@ class App extends React.Component { // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized prevState.selectedLinearElement?.elementId !== hitElement.id - ? new LinearElementEditor(hitElement) + ? new LinearElementEditor( + hitElement, + this.scene.getNonDeletedElementsMap(), + ) : prevState.selectedLinearElement, })); } @@ -9684,11 +9717,9 @@ class App extends React.Component { bindOrUnbindLinearElements( linearElements, - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), - this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? [], + this.scene, this.state.zoom, ); } @@ -9845,9 +9876,13 @@ class App extends React.Component { const dataURL = this.files[fileId]?.dataURL || (await getDataURL(imageFile)); - const imageElement = this.scene.mutateElement(_imageElement, { - fileId, - }, { informMutation: false }) as NonDeleted; + const imageElement = this.scene.mutate( + _imageElement, + { + fileId, + }, + { informMutation: false }, + ) as NonDeleted; return new Promise>( async (resolve, reject) => { @@ -9912,7 +9947,7 @@ class App extends React.Component { showCursorImagePreview, }); } catch (error: any) { - this.scene.mutateElement(imageElement, { + this.scene.mutate(imageElement, { isDeleted: true, }); this.actionManager.executeAction(actionFinalize); @@ -10058,7 +10093,7 @@ class App extends React.Component { imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value ) { const placeholderSize = 100 / this.state.zoom.value; - this.scene.mutateElement(imageElement, { + this.scene.mutate(imageElement, { x: imageElement.x - placeholderSize / 2, y: imageElement.y - placeholderSize / 2, width: placeholderSize, @@ -10092,7 +10127,7 @@ class App extends React.Component { const x = imageElement.x + imageElement.width / 2 - width / 2; const y = imageElement.y + imageElement.height / 2 - height / 2; - this.scene.mutateElement(imageElement, { + this.scene.mutate(imageElement, { x, y, width, @@ -10523,7 +10558,10 @@ class App extends React.Component { this, ), selectedLinearElement: isLinearElement(element) - ? new LinearElementEditor(element) + ? new LinearElementEditor( + element, + this.scene.getNonDeletedElementsMap(), + ) : null, } : this.state), @@ -10556,6 +10594,7 @@ class App extends React.Component { height: distance(pointerDownState.origin.y, pointerCoords.y), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldResizeFromCenter: false, + scene: this.scene, zoom: this.state.zoom.value, informMutation, }); @@ -10621,6 +10660,7 @@ class App extends React.Component { : shouldMaintainAspectRatio(event), shouldResizeFromCenter: shouldResizeFromCenter(event), zoom: this.state.zoom.value, + scene: this.scene, widthAspectRatio: aspectRatio, originOffset: this.state.originSnapOffset, informMutation, @@ -10708,7 +10748,7 @@ class App extends React.Component { transformHandleType, ); - this.scene.mutateElement( + this.scene.mutate( croppingElement, cropElement( croppingElement, @@ -10846,7 +10886,6 @@ class App extends React.Component { pointerDownState.originalElements, transformHandleType, selectedElements, - this.scene.getElementsMapIncludingDeleted(), this.scene, shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index b5491dedd..2db2578c2 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -5,11 +5,12 @@ import { CLASSES, DEFAULT_SIDEBAR, TOOL_TYPE, + arrayToMap, capitalizeString, isShallowEqual, } from "@excalidraw/common"; -import { mutateElement } from "@excalidraw/element/mutateElement"; +import { mutateElementWith } from "@excalidraw/element/mutateElement"; import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions"; @@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache"; import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; -import Scene from "../scene/Scene"; import { actionToggleStats } from "../actions"; import { trackEvent } from "../analytics"; import { isHandToolActive } from "../appState"; @@ -446,22 +446,18 @@ const LayerUI = ({ if (selectedElements.length) { for (const element of selectedElements) { - mutateElement( - element, - { - [altKey && eyeDropperState.swapPreviewOnAlt - ? colorPickerType === "elementBackground" - ? "strokeColor" - : "backgroundColor" - : colorPickerType === "elementBackground" - ? "backgroundColor" - : "strokeColor"]: color, - }, - false, - ); + mutateElementWith(element, arrayToMap(elements), { + [altKey && eyeDropperState.swapPreviewOnAlt + ? colorPickerType === "elementBackground" + ? "strokeColor" + : "backgroundColor" + : colorPickerType === "elementBackground" + ? "backgroundColor" + : "strokeColor"]: color, + }); ShapeCache.delete(element); } - Scene.getScene(selectedElements[0])?.triggerUpdate(); + app.scene.triggerUpdate(); } else if (colorPickerType === "elementBackground") { setAppState({ currentItemBackgroundColor: color, diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx index 67693551f..f3f50ef3a 100644 --- a/packages/excalidraw/components/Stats/Angle.tsx +++ b/packages/excalidraw/components/Stats/Angle.tsx @@ -1,7 +1,5 @@ import { degreesToRadians, radiansToDegrees } from "@excalidraw/math"; -import { mutateElement } from "@excalidraw/element/mutateElement"; - import { getBoundTextElement } from "@excalidraw/element/textElement"; import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks"; @@ -35,7 +33,6 @@ const handleDegreeChange: DragInputCallbackType = ({ scene, }) => { const elementsMap = scene.getNonDeletedElementsMap(); - const elements = scene.getNonDeletedElements(); const origElement = originalElements[0]; if (origElement && !isElbowArrow(origElement)) { const latestElement = elementsMap.get(origElement.id); @@ -45,14 +42,14 @@ const handleDegreeChange: DragInputCallbackType = ({ if (nextValue !== undefined) { const nextAngle = degreesToRadians(nextValue as Degrees); - mutateElement(latestElement, { + scene.mutate(latestElement, { angle: nextAngle, }); - updateBindings(latestElement, elementsMap, elements, scene); + updateBindings(latestElement, scene); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { - mutateElement(boundTextElement, { angle: nextAngle }); + scene.mutate(boundTextElement, { angle: nextAngle }); } return; @@ -71,14 +68,14 @@ const handleDegreeChange: DragInputCallbackType = ({ const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); - mutateElement(latestElement, { + scene.mutate(latestElement, { angle: nextAngle, }); - updateBindings(latestElement, elementsMap, elements, scene); + updateBindings(latestElement, scene); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { - mutateElement(boundTextElement, { angle: nextAngle }); + scene.mutate(boundTextElement, { angle: nextAngle }); } } }; diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index 142abc407..fb93d3ec0 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -5,7 +5,6 @@ import { MINIMAL_CROP_SIZE, getUncroppedWidthAndHeight, } from "@excalidraw/element/cropElement"; -import { mutateElement } from "@excalidraw/element/mutateElement"; import { resizeSingleElement } from "@excalidraw/element/resizeElements"; import { isImageElement } from "@excalidraw/element/typeChecks"; @@ -113,7 +112,7 @@ const handleDimensionChange: DragInputCallbackType< }; } - mutateElement(element, { + scene.mutate(element, { crop: nextCrop, width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), @@ -144,7 +143,7 @@ const handleDimensionChange: DragInputCallbackType< height: nextCropHeight, }; - mutateElement(element, { + scene.mutate(element, { crop: nextCrop, width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), @@ -176,8 +175,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldMaintainAspectRatio: keepAspectRatio, @@ -223,8 +222,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldMaintainAspectRatio: keepAspectRatio, diff --git a/packages/excalidraw/components/Stats/FontSize.tsx b/packages/excalidraw/components/Stats/FontSize.tsx index 90bdee564..bdbbebd89 100644 --- a/packages/excalidraw/components/Stats/FontSize.tsx +++ b/packages/excalidraw/components/Stats/FontSize.tsx @@ -75,6 +75,7 @@ const handleFontSizeChange: DragInputCallbackType< latestElement, scene.getContainerElement(latestElement), scene.getNonDeletedElementsMap(), + (...args) => scene.mutate(...args), ); } } diff --git a/packages/excalidraw/components/Stats/MultiAngle.tsx b/packages/excalidraw/components/Stats/MultiAngle.tsx index 3cabd19c0..895fe5cff 100644 --- a/packages/excalidraw/components/Stats/MultiAngle.tsx +++ b/packages/excalidraw/components/Stats/MultiAngle.tsx @@ -54,17 +54,13 @@ const handleDegreeChange: DragInputCallbackType< if (!element) { continue; } - mutateElement( - element, - { - angle: nextAngle, - }, - false, - ); + mutateElement(element, { + angle: nextAngle, + }); const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && !isArrowElement(element)) { - mutateElement(boundTextElement, { angle: nextAngle }, false); + mutateElement(boundTextElement, { angle: nextAngle }); } } @@ -92,17 +88,13 @@ const handleDegreeChange: DragInputCallbackType< const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees); - mutateElement( - latestElement, - { - angle: nextAngle, - }, - false, - ); + mutateElement(latestElement, { + angle: nextAngle, + }); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { - mutateElement(boundTextElement, { angle: nextAngle }, false); + mutateElement(boundTextElement, { angle: nextAngle }); } } scene.triggerUpdate(); diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index 301eb1e79..cc771b56f 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -3,7 +3,6 @@ import { useMemo } from "react"; import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common"; import { updateBoundElements } from "@excalidraw/element/binding"; -import { mutateElement } from "@excalidraw/element/mutateElement"; import { rescalePointsInElement, resizeSingleElement, @@ -13,11 +12,14 @@ import { handleBindTextResize, } from "@excalidraw/element/textElement"; -import { isElbowArrow, isTextElement } from "@excalidraw/element/typeChecks"; +import { isTextElement } from "@excalidraw/element/typeChecks"; import { getCommonBounds } from "@excalidraw/utils"; -import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; +import { + mutateElement, + mutateElementWith, +} from "@excalidraw/element/mutateElement"; import type { ElementsMap, @@ -82,11 +84,7 @@ const resizeElementInGroup = ( ) => { const updates = getResizedUpdates(anchorX, anchorY, scale, origElement); - if (isElbowArrow(latestElement)) { - mutateElbowArrow(latestElement, updates, false, elementsMap); - } else { - mutateElement(latestElement, updates, false); - } + mutateElementWith(latestElement, elementsMap, updates); const boundTextElement = getBoundTextElement( origElement, @@ -99,13 +97,9 @@ const resizeElementInGroup = ( }); const latestBoundTextElement = elementsMap.get(boundTextElement.id); if (latestBoundTextElement && isTextElement(latestBoundTextElement)) { - mutateElement( - latestBoundTextElement, - { - fontSize: newFontSize, - }, - false, - ); + mutateElement(latestBoundTextElement, { + fontSize: newFontSize, + }); handleBindTextResize( latestElement, elementsMap, @@ -244,8 +238,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldInformMutation: false, @@ -347,8 +341,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldInformMutation: false, diff --git a/packages/excalidraw/components/Stats/MultiFontSize.tsx b/packages/excalidraw/components/Stats/MultiFontSize.tsx index 6bac4bd3c..1060f2529 100644 --- a/packages/excalidraw/components/Stats/MultiFontSize.tsx +++ b/packages/excalidraw/components/Stats/MultiFontSize.tsx @@ -84,19 +84,15 @@ const handleFontSizeChange: DragInputCallbackType< nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE); for (const textElement of latestTextElements) { - mutateElement( - textElement, - { - fontSize: nextFontSize, - }, - false, - ); + mutateElement(textElement, { + fontSize: nextFontSize, + }); redrawTextBoundingBox( textElement, scene.getContainerElement(textElement), elementsMap, - false, + (...args) => scene.mutate(...args), ); } @@ -117,19 +113,15 @@ const handleFontSizeChange: DragInputCallbackType< if (shouldChangeByStepSize) { nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); } - mutateElement( - latestElement, - { - fontSize: nextFontSize, - }, - false, - ); + mutateElement(latestElement, { + fontSize: nextFontSize, + }); redrawTextBoundingBox( latestElement, scene.getContainerElement(latestElement), elementsMap, - false, + (...args) => scene.mutate(...args), ); } diff --git a/packages/excalidraw/components/Stats/MultiPosition.tsx b/packages/excalidraw/components/Stats/MultiPosition.tsx index 98058efec..a6931d6dc 100644 --- a/packages/excalidraw/components/Stats/MultiPosition.tsx +++ b/packages/excalidraw/components/Stats/MultiPosition.tsx @@ -5,12 +5,7 @@ import { isTextElement } from "@excalidraw/element/typeChecks"; import { getCommonBounds } from "@excalidraw/element/bounds"; -import type { - ElementsMap, - ExcalidrawElement, - NonDeletedExcalidrawElement, - NonDeletedSceneElementsMap, -} from "@excalidraw/element/types"; +import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import StatsDragInput from "./DragInput"; import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; @@ -36,13 +31,11 @@ const moveElements = ( property: MultiPositionProps["property"], changeInTopX: number, changeInTopY: number, - elements: readonly ExcalidrawElement[], originalElements: readonly ExcalidrawElement[], - elementsMap: NonDeletedSceneElementsMap, originalElementsMap: ElementsMap, scene: Scene, ) => { - for (let i = 0; i < elements.length; i++) { + for (let i = 0; i < originalElements.length; i++) { const origElement = originalElements[i]; const [cx, cy] = [ @@ -65,8 +58,6 @@ const moveElements = ( newTopLeftX, newTopLeftY, origElement, - elementsMap, - elements, scene, originalElementsMap, false, @@ -78,11 +69,10 @@ const moveGroupTo = ( nextX: number, nextY: number, originalElements: ExcalidrawElement[], - elementsMap: NonDeletedSceneElementsMap, - elements: readonly NonDeletedExcalidrawElement[], originalElementsMap: ElementsMap, scene: Scene, ) => { + const elementsMap = scene.getNonDeletedElementsMap(); const [x1, y1, ,] = getCommonBounds(originalElements); const offsetX = nextX - x1; const offsetY = nextY - y1; @@ -112,8 +102,6 @@ const moveGroupTo = ( topLeftX + offsetX, topLeftY + offsetY, origElement, - elementsMap, - elements, scene, originalElementsMap, false, @@ -135,7 +123,6 @@ const handlePositionChange: DragInputCallbackType< originalAppState, }) => { const elementsMap = scene.getNonDeletedElementsMap(); - const elements = scene.getNonDeletedElements(); if (nextValue !== undefined) { for (const atomicUnit of getAtomicUnits( @@ -159,8 +146,6 @@ const handlePositionChange: DragInputCallbackType< newTopLeftX, newTopLeftY, elementsInUnit.map((el) => el.original), - elementsMap, - elements, originalElementsMap, scene, ); @@ -188,8 +173,6 @@ const handlePositionChange: DragInputCallbackType< newTopLeftX, newTopLeftY, origElement, - elementsMap, - elements, scene, originalElementsMap, false, @@ -214,8 +197,6 @@ const handlePositionChange: DragInputCallbackType< changeInTopX, changeInTopY, originalElements, - originalElements, - elementsMap, originalElementsMap, scene, ); diff --git a/packages/excalidraw/components/Stats/Position.tsx b/packages/excalidraw/components/Stats/Position.tsx index bf6dfd161..7e1bc6529 100644 --- a/packages/excalidraw/components/Stats/Position.tsx +++ b/packages/excalidraw/components/Stats/Position.tsx @@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ originalAppState, }) => { const elementsMap = scene.getNonDeletedElementsMap(); - const elements = scene.getNonDeletedElements(); const origElement = originalElements[0]; const [cx, cy] = [ origElement.x + origElement.width / 2, @@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ newTopLeftX, newTopLeftY, origElement, - elementsMap, - elements, scene, originalElementsMap, ); @@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ newTopLeftX, newTopLeftY, origElement, - elementsMap, - elements, scene, originalElementsMap, ); diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index dbb47a234..374b03d41 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -4,7 +4,6 @@ import { bindOrUnbindLinearElements, updateBoundElements, } from "@excalidraw/element/binding"; -import { mutateElement } from "@excalidraw/element/mutateElement"; import { getBoundTextElement } from "@excalidraw/element/textElement"; import { isFrameLikeElement, @@ -24,7 +23,6 @@ import type { ElementsMap, ExcalidrawElement, NonDeletedExcalidrawElement, - NonDeletedSceneElementsMap, } from "@excalidraw/element/types"; import type Scene from "../../scene/Scene"; @@ -119,12 +117,11 @@ export const moveElement = ( newTopLeftX: number, newTopLeftY: number, originalElement: ExcalidrawElement, - elementsMap: NonDeletedSceneElementsMap, - elements: readonly NonDeletedExcalidrawElement[], scene: Scene, originalElementsMap: ElementsMap, shouldInformMutation = true, ) => { + const elementsMap = scene.getNonDeletedElementsMap(); const latestElement = elementsMap.get(originalElement.id); if (!latestElement) { return; @@ -148,15 +145,15 @@ export const moveElement = ( -originalElement.angle as Radians, ); - mutateElement( + scene.mutate( latestElement, { x, y, }, - shouldInformMutation, + { informMutation: shouldInformMutation }, ); - updateBindings(latestElement, elementsMap, elements, scene); + updateBindings(latestElement, scene); const boundTextElement = getBoundTextElement( originalElement, @@ -165,13 +162,13 @@ export const moveElement = ( if (boundTextElement) { const latestBoundTextElement = elementsMap.get(boundTextElement.id); latestBoundTextElement && - mutateElement( + scene.mutate( latestBoundTextElement, { x: boundTextElement.x + changeInX, y: boundTextElement.y + changeInY, }, - shouldInformMutation, + { informMutation: shouldInformMutation }, ); } }; @@ -199,8 +196,6 @@ export const getAtomicUnits = ( export const updateBindings = ( latestElement: ExcalidrawElement, - elementsMap: NonDeletedSceneElementsMap, - elements: readonly NonDeletedExcalidrawElement[], scene: Scene, options?: { simultaneouslyUpdated?: readonly ExcalidrawElement[]; @@ -209,16 +204,12 @@ export const updateBindings = ( }, ) => { if (isLinearElement(latestElement)) { - bindOrUnbindLinearElements( - [latestElement], - elementsMap, - elements, - scene, - true, - [], - options?.zoom, - ); + bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom); } else { - updateBoundElements(latestElement, elementsMap, options); + updateBoundElements( + latestElement, + scene.getNonDeletedElementsMap(), + options, + ); } }; diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 15ad1ffde..f9beaf226 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -38,6 +38,10 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement"; import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; +import { mutateElementWith } from "@excalidraw/element/mutateElement"; + +import { getCommonBounds } from "@excalidraw/element/bounds"; + import type { ElementConstructorOpts } from "@excalidraw/element/newElement"; import type { @@ -63,8 +67,6 @@ import type { import type { MarkOptional } from "@excalidraw/common/utility-types"; -import { getCommonBounds } from ".."; - export type ValidLinearElement = { type: "arrow" | "line"; x: number; @@ -240,7 +242,12 @@ const bindTextToContainer = ( }), }); - redrawTextBoundingBox(textElement, container, elementsMap); + redrawTextBoundingBox( + textElement, + container, + elementsMap, + (element, updates) => mutateElementWith(element, elementsMap, updates), + ); return [container, textElement] as const; }; @@ -336,6 +343,7 @@ const bindLinearElementToElement = ( startBoundElement as ExcalidrawBindableElement, "start", elementsMap, + (element, updates) => mutateElementWith(element, elementsMap, updates), ); } } @@ -411,6 +419,7 @@ const bindLinearElementToElement = ( endBoundElement as ExcalidrawBindableElement, "end", elementsMap, + (element, updates) => mutateElementWith(element, elementsMap, updates), ); } } diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index 0f68ef07c..221977fa8 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -259,13 +259,12 @@ export { } from "@excalidraw/common"; export { + mutateElementWith, mutateElement, newElementWith, bumpVersion, } from "@excalidraw/element/mutateElement"; -export { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; - export { CaptureUpdateAction } from "./store"; export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library"; diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts index d59b2d743..f7e791665 100644 --- a/packages/excalidraw/lasso/index.ts +++ b/packages/excalidraw/lasso/index.ts @@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail { this.app.scene.getNonDeletedElement( selectedIds[0], ) as NonDeleted, + this.app.scene.getNonDeletedElementsMap(), ) : null, }; diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index 489973654..971d1324d 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -8,10 +8,7 @@ import { isTestEnv, } from "@excalidraw/common"; import { isNonDeletedElement } from "@excalidraw/element"; -import { - isElbowArrow, - isFrameLikeElement, -} from "@excalidraw/element/typeChecks"; +import { isFrameLikeElement } from "@excalidraw/element/typeChecks"; import { getElementsInGroup } from "@excalidraw/element/groups"; import { @@ -23,12 +20,10 @@ import { import { getSelectedElements } from "@excalidraw/element/selection"; import { - mutateElement, + mutateElementWith, type ElementUpdate, } from "@excalidraw/element/mutateElement"; -import { mutateElbowArrow } from "@excalidraw/element/elbowArrow"; - import type { ExcalidrawElement, NonDeletedExcalidrawElement, @@ -39,7 +34,6 @@ import type { NonDeletedSceneElementsMap, OrderedExcalidrawElement, Ordered, - ExcalidrawElbowArrowElement, } from "@excalidraw/element/types"; import type { @@ -424,8 +418,12 @@ class Scene { }; // TODO_SCENE: should be accessed as app.scene through the API + // TODO_SCENE: in theory actions (and most of the App handlers) should not needs this as they are ending with "replaceAllElements" anyway // TODO_SCENE: inform mutation false is the new default, meaning all mutateElement with nothing should likely use scene instead - mutateElement>( + // 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>( element: TElement, updates: ElementUpdate, options: { @@ -435,16 +433,9 @@ class Scene { informMutation: true, }, ) { - if (isElbowArrow(element)) { - mutateElbowArrow( - element, - updates as ElementUpdate, - this.getNonDeletedElementsMap(), - options, - ); - } else { - mutateElement(element, updates); - } + const elementsMap = this.getNonDeletedElementsMap(); + + mutateElementWith(element, elementsMap, updates, options); if (options.informMutation) { this.triggerUpdate(); diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 09aa308a5..6eb76b199 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -424,7 +424,6 @@ export class API { { boundElements: [{ type: "text", id: text.id }], }, - false, ); return [rectangle, text]; @@ -459,7 +458,6 @@ export class API { { boundElements: [{ type: "text", id: text.id }], }, - false, ); return [arrow, text]; diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index 861998584..57c5d967a 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -177,7 +177,7 @@ describe("Test Linear Elements", () => { pointFrom(0.5, 0), pointFrom(100, 100), ]); - new LinearElementEditor(element); + new LinearElementEditor(element, arrayToMap(h.elements)); expect(element.points).toEqual([ pointFrom(0, 0), pointFrom(99.5, 100), diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 77fc7e57d..5bef4638b 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -13,8 +13,6 @@ import type { ExcalidrawRectangleElement, } from "@excalidraw/element/types"; -import type Scene from "@excalidraw/excalidraw/scene/Scene"; - import { Excalidraw } from "../index"; import * as InteractiveCanvas from "../renderer/interactiveScene"; import * as StaticScene from "../renderer/staticScene"; @@ -85,15 +83,13 @@ describe("move element", () => { const rectA = UI.createElement("rectangle", { size: 100 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); - const elementsMap = h.app.scene.getNonDeletedElementsMap(); act(() => { // bind line to two rectangles bindOrUnbindLinearElement( arrow.get() as NonDeleted, rectA.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement, - elementsMap, - {} as Scene, + h.app.scene, ); }); diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.tsx index b1610125f..5f0e3134d 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.tsx @@ -45,7 +45,6 @@ import type { import { actionSaveToActiveFile } from "../actions"; -import Scene from "../scene/Scene"; import { parseClipboard } from "../clipboard"; import { actionDecreaseFontSize, @@ -130,8 +129,7 @@ export const textWysiwyg = ({ const updateWysiwygStyle = () => { const appState = app.state; - const updatedTextElement = - Scene.getScene(element)?.getElement(id); + const updatedTextElement = app.scene.getElement(id); if (!updatedTextElement) { return; @@ -544,7 +542,7 @@ export const textWysiwyg = ({ // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // wysiwyg on update cleanup(); - const updateElement = Scene.getScene(element)?.getElement( + const updateElement = app.scene.getElement( element.id, ) as ExcalidrawTextElement; if (!updateElement) { @@ -583,6 +581,7 @@ export const textWysiwyg = ({ updateElement, container, app.scene.getNonDeletedElementsMap(), + (...args) => app.scene.mutate(...args), ); }