diff --git a/packages/element/src/align.ts b/packages/element/src/align.ts index 61535b4348..30f60b2c30 100644 --- a/packages/element/src/align.ts +++ b/packages/element/src/align.ts @@ -15,13 +15,12 @@ export interface Alignment { export const alignElements = ( selectedElements: ExcalidrawElement[], - elementsMap: ElementsMap, alignment: Alignment, scene: Scene, ): ExcalidrawElement[] => { const groups: ExcalidrawElement[][] = getMaximumGroups( selectedElements, - elementsMap, + scene.getNonDeletedElementsMap(), ); const selectionBoundingBox = getCommonBoundingBox(selectedElements); @@ -33,12 +32,12 @@ export const alignElements = ( ); return group.map((element) => { // update element - const updatedEle = mutateElement(element, { + const updatedEle = scene.mutateElement(element, { x: element.x + translation.x, y: element.y + translation.y, }); // update bound elements - updateBoundElements(element, scene.getNonDeletedElementsMap(), { + updateBoundElements(scene, element, { simultaneouslyUpdated: group, }); return updatedEle; diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 410d466242..076a6ea3b6 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -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.mutateElement(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, scene, startOrEnd); if (unbound != null) { unboundFromElementIds.add(unbound); } @@ -209,16 +207,11 @@ const bindOrUnbindLinearElementEdge = ( : startOrEnd === "start" || otherEdgeBindableElement.id !== bindableElement.id) ) { - bindLinearElement( - linearElement, - bindableElement, - startOrEnd, - elementsMap, - ); + bindLinearElement(linearElement, bindableElement, startOrEnd, scene); boundToElementIds.add(bindableElement.id); } } else { - bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap); + bindLinearElement(linearElement, bindableElement, startOrEnd, scene); boundToElementIds.add(bindableElement.id); } }; @@ -362,7 +355,6 @@ const getBindingStrategyForDraggingArrowOrJoints = ( export const bindOrUnbindLinearElements = ( selectedElements: NonDeleted[], - elementsMap: NonDeletedSceneElementsMap, elements: readonly NonDeletedExcalidrawElement[], scene: Scene, isBindingEnabled: boolean, @@ -376,20 +368,20 @@ export const bindOrUnbindLinearElements = ( selectedElement, isBindingEnabled, draggingPoints ?? [], - elementsMap, + scene.getNonDeletedElementsMap(), elements, zoom, ) : // The arrow itself (the shaft) or the inner joins are dragged getBindingStrategyForDraggingArrowOrJoints( selectedElement, - elementsMap, + scene.getNonDeletedElementsMap(), elements, isBindingEnabled, zoom, ); - bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene); + bindOrUnbindLinearElement(selectedElement, start, end, scene); }); }; @@ -429,22 +421,21 @@ 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, ); } const hoveredElement = getHoveredElementForBinding( pointerCoords, - elements, - elementsMap, + scene.getNonDeletedElements(), + scene.getNonDeletedElementsMap(), appState.zoom, isElbowArrow(linearElement), isElbowArrow(linearElement), @@ -458,7 +449,7 @@ export const maybeBindLinearElement = ( "end", ) ) { - bindLinearElement(linearElement, hoveredElement, "end", elementsMap); + bindLinearElement(linearElement, hoveredElement, "end", scene); } } }; @@ -487,7 +478,7 @@ export const bindLinearElement = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", - elementsMap: NonDeletedSceneElementsMap, + scene: Scene, ): void => { if (!isArrowElement(linearElement)) { return; @@ -500,7 +491,7 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, - elementsMap, + scene.getNonDeletedElementsMap(), ), hoveredElement, ), @@ -513,18 +504,17 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, - elementsMap, ), }; } - mutateElement(linearElement, { + scene.mutateElement(linearElement, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, }); const boundElementsMap = arrayToMap(hoveredElement.boundElements || []); if (!boundElementsMap.has(linearElement.id)) { - mutateElement(hoveredElement, { + scene.mutateElement(hoveredElement, { boundElements: (hoveredElement.boundElements || []).concat({ id: linearElement.id, type: "arrow", @@ -565,6 +555,7 @@ const isLinearElementSimple = ( const unbindLinearElement = ( linearElement: NonDeleted, + scene: Scene, startOrEnd: "start" | "end", ): ExcalidrawBindableElement["id"] | null => { const field = startOrEnd === "start" ? "startBinding" : "endBinding"; @@ -572,7 +563,7 @@ const unbindLinearElement = ( if (binding == null) { return null; } - mutateElement(linearElement, { [field]: null }); + scene.mutateElement(linearElement, { [field]: null }); return binding.elementId; }; @@ -739,8 +730,8 @@ const calculateFocusAndGap = ( // done before the `changedElement` is updated, and the `newSize` is passed // in explicitly. export const updateBoundElements = ( + scene: Scene, changedElement: NonDeletedExcalidrawElement, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, options?: { simultaneouslyUpdated?: readonly ExcalidrawElement[]; newSize?: { width: number; height: number }; @@ -756,6 +747,7 @@ export const updateBoundElements = ( return; } + const elementsMap = scene.getNonDeletedElementsMap(); boundElementsVisitor(elementsMap, changedElement, (element) => { if (!isLinearElement(element) || element.isDeleted) { return; @@ -796,20 +788,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); - } - + scene.mutateElement(element, bindings); return; } @@ -898,7 +877,6 @@ export const getHeadingForElbowArrowSnap = ( otherPoint: Readonly, bindableElement: ExcalidrawBindableElement | undefined | null, aabb: Bounds | undefined | null, - elementsMap: ElementsMap, origPoint: GlobalPoint, zoom?: AppState["zoom"], ): Heading => { @@ -908,12 +886,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 +906,6 @@ export const getHeadingForElbowArrowSnap = ( const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, - elementsMap: ElementsMap, zoom?: AppState["zoom"], ) => { const distance = distanceToBindableElement(bindableElement, point); @@ -1239,7 +1211,6 @@ const updateBoundPoint = ( linearElement, bindableElement, startOrEnd === "startBinding" ? "start" : "end", - elementsMap, ).fixedPoint; const globalMidPoint = pointFrom( bindableElement.x + bindableElement.width / 2, @@ -1349,7 +1320,6 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", - elementsMap: ElementsMap, ): { fixedPoint: FixedPoint } => { const bounds = [ hoveredElement.x, @@ -2166,6 +2136,7 @@ export class BoundElement { * NOTE: rebind expects that affected elements were previously unbound with `BoundElement.unbindAffected` */ public static rebindAffected = ( + scene: Scene, elements: ElementsMap, boundElement: ExcalidrawElement | undefined, updateElementWith: ( diff --git a/packages/element/src/dragElements.ts b/packages/element/src/dragElements.ts index 669417a54e..98c13430d1 100644 --- a/packages/element/src/dragElements.ts +++ b/packages/element/src/dragElements.ts @@ -104,7 +104,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 +112,14 @@ export const dragSelectedElements = ( scene.getNonDeletedElementsMap(), ); if (textElement) { - updateElementCoords(pointerDownState, textElement, adjustedOffset); + updateElementCoords( + pointerDownState, + textElement, + scene, + adjustedOffset, + ); } - updateBoundElements(element, scene.getElementsMapIncludingDeleted(), { + updateBoundElements(scene, element, { simultaneouslyUpdated: Array.from(elementsToUpdate), }); } @@ -155,6 +160,7 @@ const calculateOffset = ( const updateElementCoords = ( pointerDownState: PointerDownState, element: NonDeletedExcalidrawElement, + scene: Scene, dragOffset: { x: number; y: number }, ) => { const originalElement = @@ -163,7 +169,7 @@ const updateElementCoords = ( const nextX = originalElement.x + dragOffset.x; const nextY = originalElement.y + dragOffset.y; - mutateElement(element, { + scene.mutateElement(element, { x: nextX, y: nextY, }); @@ -190,6 +196,7 @@ export const dragNewElement = ({ shouldMaintainAspectRatio, shouldResizeFromCenter, zoom, + scene, widthAspectRatio = null, originOffset = null, informMutation = true, @@ -205,6 +212,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 +293,7 @@ export const dragNewElement = ({ }; } - mutateElement( + scene.mutateElement( newElement, { x: newX + (originOffset?.x ?? 0), @@ -295,7 +303,7 @@ export const dragNewElement = ({ ...textAutoResize, ...imageInitialDimension, }, - informMutation, + { informMutation }, ); } }; diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index e95ffc2062..663270a237 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -1329,14 +1329,12 @@ const getElbowArrowData = ( const startHeading = getBindPointHeading( startGlobalPoint, endGlobalPoint, - elementsMap, hoveredStartElement, origStartGlobalPoint, ); const endHeading = getBindPointHeading( endGlobalPoint, startGlobalPoint, - elementsMap, hoveredEndElement, origEndGlobalPoint, ); @@ -2306,7 +2304,6 @@ const getGlobalPoint = ( const getBindPointHeading = ( p: GlobalPoint, otherPoint: GlobalPoint, - elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, hoveredElement: ExcalidrawBindableElement | null | undefined, origPoint: GlobalPoint, ): Heading => @@ -2324,7 +2321,6 @@ const getBindPointHeading = ( number, ], ), - elementsMap, origPoint, ); diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index 8ee2ba530a..846256278d 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; @@ -440,18 +447,8 @@ const createBindingArrow = ( elbowed: true, }); - bindLinearElement( - bindingArrow, - startBindingElement, - "start", - elementsMap as NonDeletedSceneElementsMap, - ); - bindLinearElement( - bindingArrow, - endBindingElement, - "end", - elementsMap as NonDeletedSceneElementsMap, - ); + bindLinearElement(bindingArrow, startBindingElement, "start", scene); + bindLinearElement(bindingArrow, endBindingElement, "end", scene); const changedElements = new Map(); changedElements.set( @@ -635,6 +632,7 @@ export class FlowChartCreator { elementsMap: ElementsMap, appState: AppState, direction: LinkDirection, + scene: Scene, ) { if (direction !== this.direction) { const { nextNode, bindingArrow } = addNewNode( @@ -642,6 +640,7 @@ export class FlowChartCreator { elementsMap, appState, direction, + scene, ); this.numberOfNodes = 1; @@ -655,6 +654,7 @@ export class FlowChartCreator { elementsMap, appState, direction, + scene, this.numberOfNodes, ); @@ -682,13 +682,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/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 5a0e1e1efb..cfb29e084c 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -125,13 +125,13 @@ export class LinearElementEditor { public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly elbowed: boolean; - constructor(element: NonDeleted) { + constructor(element: NonDeleted, scene: Scene) { 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, scene); } this.selectedPointsIndices = null; @@ -796,7 +796,7 @@ export class LinearElementEditor { linearElementEditor.lastUncommittedPoint == null && !isElbowArrow(element) ) { - mutateElement(element, { + scene.mutateElement(element, { points: [ ...element.points, LinearElementEditor.createPointAt( @@ -862,7 +862,6 @@ export class LinearElementEditor { element, startBindingElement, endBindingElement, - elementsMap, scene, ); } @@ -1165,19 +1164,23 @@ export class LinearElementEditor { // element-mutating methods // --------------------------------------------------------------------------- - static normalizePoints(element: NonDeleted) { - mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); + static normalizePoints( + element: NonDeleted, + scene: Scene, + ) { + scene.mutateElement( + element, + 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 +1223,7 @@ export class LinearElementEditor { return acc; }, []); - const updates = { points: nextPoints }; - if (isElbowArrow(element)) { - mutateElbowArrow(element, updates, true, elementsMap); - } else { - mutateElement(element, updates); - } + scene.mutateElement(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 +1398,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 +1430,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.mutateElement(element, { points }); ret.pointerDownState = { ...linearElementEditor.pointerDownState, @@ -1489,7 +1483,7 @@ export class LinearElementEditor { updates.points = Array.from(nextPoints); if (!options?.sceneElementsMap) { - mutateElbowArrow(element, updates, true, options?.sceneElementsMap!, { + mutateElbowArrow(element, updates, options?.sceneElementsMap!, { isDragging: options?.isDragging, }); } else { @@ -1790,8 +1784,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 +1829,9 @@ export class LinearElementEditor { .map((segment) => segment.index) .reduce((count, idx) => (idx < index ? count + 1 : count), 0); - mutateElbowArrow( - element, - { - fixedSegments: nextFixedSegments, - }, - true, - elementsMap, - ); + scene.mutateElement(element, { + fixedSegments: nextFixedSegments, + }); const point = pointFrom( element.x + @@ -1873,19 +1863,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.mutateElement(element, { + fixedSegments: element.fixedSegments?.filter( + (segment) => segment.index !== index, + ), + }); } } diff --git a/packages/element/src/resizeElements.ts b/packages/element/src/resizeElements.ts index f7321aad82..c70000e8cd 100644 --- a/packages/element/src/resizeElements.ts +++ b/packages/element/src/resizeElements.ts @@ -85,7 +85,6 @@ export const transformElements = ( originalElements: PointerDownState["originalElements"], transformHandleType: MaybeTransformHandleType, selectedElements: readonly NonDeletedExcalidrawElement[], - elementsMap: SceneElementsMap, scene: Scene, shouldRotateWithDiscreteAngle: boolean, shouldResizeFromCenter: boolean, @@ -95,35 +94,35 @@ 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, shouldRotateWithDiscreteAngle, ); - updateBoundElements(element, elementsMap); + updateBoundElements(scene, element); } } else if (isTextElement(element) && transformHandleType) { resizeSingleTextElement( originalElements, element, - elementsMap, + scene.getNonDeletedElementsMap(), transformHandleType, shouldResizeFromCenter, pointerX, pointerY, ); - updateBoundElements(element, elementsMap); + updateBoundElements(scene, element); return true; } else if (transformHandleType) { const elementId = selectedElements[0].id; - const latestElement = elementsMap.get(elementId); + const latestElement = scene.getNonDeletedElementsMap().get(elementId); const origElement = originalElements.get(elementId); if (latestElement && origElement) { @@ -131,7 +130,7 @@ export const transformElements = ( getNextSingleWidthAndHeightFromPointer( latestElement, origElement, - elementsMap, + scene.getNonDeletedElementsMap(), originalElements, transformHandleType, pointerX, @@ -147,8 +146,8 @@ export const transformElements = ( nextHeight, latestElement, origElement, - elementsMap, originalElements, + scene, transformHandleType, { shouldMaintainAspectRatio, @@ -163,7 +162,6 @@ export const transformElements = ( rotateMultipleElements( originalElements, selectedElements, - elementsMap, scene, pointerX, pointerY, @@ -212,13 +210,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 +235,13 @@ const rotateSingleElement = ( } const boundTextElementId = getBoundTextElementId(element); - mutateElement(element, { angle }); + scene.mutateElement(element, { angle }); if (boundTextElementId) { const textElement = scene.getElement(boundTextElementId); if (textElement && !isArrowElement(element)) { - mutateElement(textElement, { angle }); + scene.mutateElement(textElement, { angle }); } } }; @@ -517,7 +517,6 @@ const resizeSingleTextElement = ( const rotateMultipleElements = ( originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], - elementsMap: SceneElementsMap, scene: Scene, pointerX: number, pointerY: number, @@ -525,6 +524,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) { @@ -552,36 +552,27 @@ const rotateMultipleElements = ( { 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, - ); + mutateElement(element, { + x: element.x + (rotatedCX - cx), + y: element.y + (rotatedCY - cy), + angle: normalizeRadians((centerAngle + origAngle) as Radians), + }); } - updateBoundElements(element, elementsMap, { + updateBoundElements(scene, element, { simultaneouslyUpdated: elements, }); 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, - ); + mutateElement(boundText, { + x: boundText.x + (rotatedCX - cx), + y: boundText.y + (rotatedCY - cy), + angle: normalizeRadians((centerAngle + origAngle) as Radians), + }); } } } @@ -826,8 +817,8 @@ export const resizeSingleElement = ( nextHeight: number, latestElement: ExcalidrawElement, origElement: ExcalidrawElement, - elementsMap: ElementsMap, originalElementsMap: ElementsMap, + scene: Scene, handleDirection: TransformHandleDirection, { shouldInformMutation = true, @@ -840,7 +831,10 @@ export const resizeSingleElement = ( } = {}, ) => { let boundTextFont: { fontSize?: number } = {}; - const boundTextElement = getBoundTextElement(latestElement, elementsMap); + const boundTextElement = getBoundTextElement( + latestElement, + scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { const stateOfBoundTextElementAtResize = originalElementsMap.get( @@ -860,7 +854,7 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, - elementsMap, + scene.getNonDeletedElementsMap(), getBoundTextMaxWidth(updatedElement, boundTextElement), ); if (nextFont === null) { @@ -939,7 +933,7 @@ export const resizeSingleElement = ( } if ("scale" in latestElement && "scale" in origElement) { - mutateElement(latestElement, { + scene.mutateElement(latestElement, { scale: [ // defaulting because scaleX/Y can be 0/-0 (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0], @@ -974,21 +968,23 @@ export const resizeSingleElement = ( ...rescaledPoints, }; - mutateElement(latestElement, updates, shouldInformMutation); + scene.mutateElement(latestElement, updates, { + informMutation: shouldInformMutation, + }); - updateBoundElements(latestElement, elementsMap as SceneElementsMap, { + updateBoundElements(scene, latestElement, { // TODO: confirm with MARK if this actually makes sense newSize: { width: nextWidth, height: nextHeight }, }); if (boundTextElement && boundTextFont != null) { - mutateElement(boundTextElement, { + scene.mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize, }); } handleBindTextResize( latestElement, - elementsMap, + scene.getNonDeletedElementsMap(), handleDirection, shouldMaintainAspectRatio, ); @@ -1534,26 +1530,22 @@ export const resizeMultipleElements = ( } of elementsAndUpdates) { const { width, height, angle } = update; - scene.mutateElement(element, update, false, { + scene.mutateElement(element, update, { // needed for the fixed binding point udpate to take effect isDragging: true, }); - updateBoundElements(element, elementsMap as SceneElementsMap, { + updateBoundElements(scene, element, { 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.mutateElement(boundTextElement, { + fontSize: boundTextFontSize, + angle: isLinearElement(element) ? undefined : angle, + }); handleBindTextResize(element, elementsMap, handleDirection, true); } } diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx index 46023d61ea..0ef938c67f 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/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx index d861390b3a..668dc8baff 100644 --- a/packages/excalidraw/actions/actionDeleteSelected.tsx +++ b/packages/excalidraw/actions/actionDeleteSelected.tsx @@ -289,6 +289,7 @@ export const actionDeleteSelected = register({ deleteSelectedElements(elements, appState, app); fixBindingsAfterDeletion( + app.scene, nextElements, nextElements.filter((el) => el.isDeleted), ); diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index 231fe1913b..33189c7a61 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -36,6 +36,8 @@ import { CaptureUpdateAction } from "../store"; import { register } from "./register"; +import type Scene from "../scene/Scene"; + export const actionDuplicateSelection = register({ name: "duplicateSelection", label: "labels.duplicateSelection", @@ -52,7 +54,7 @@ export const actionDuplicateSelection = register({ try { const newAppState = LinearElementEditor.duplicateSelectedPoints( appState, - app.scene.getNonDeletedElementsMap(), + app.scene, ); return { @@ -95,7 +97,7 @@ export const actionDuplicateSelection = register({ elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)), appState: { ...appState, - ...updateLinearElementEditors(duplicatedElements), + ...updateLinearElementEditors(duplicatedElements, app.scene), ...selectGroupsForSelectedElements( { editingGroupId: appState.editingGroupId, @@ -131,7 +133,10 @@ export const actionDuplicateSelection = register({ ), }); -const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => { +const updateLinearElementEditors = ( + clonedElements: ExcalidrawElement[], + scene: Scene, +) => { const linears = clonedElements.filter(isLinearElement); if (linears.length === 1) { const linear = linears[0]; @@ -142,7 +147,7 @@ const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => { if (onlySingleLinearSelected) { return { - selectedLinearElement: new LinearElementEditor(linear), + selectedLinearElement: new LinearElementEditor(linear, scene), }; } } diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f78592326c..310fd2cb7c 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -56,7 +56,6 @@ export const actionFinalize = register({ element, startBindingElement, endBindingElement, - elementsMap, scene, ); } @@ -82,7 +81,11 @@ export const actionFinalize = register({ scene.getElement(appState.pendingImageElementId); if (pendingImageElement) { - mutateElement(pendingImageElement, { isDeleted: true }, false); + scene.mutateElement( + pendingImageElement, + { isDeleted: true }, + { informMutation: false }, + ); } if (window.document.activeElement instanceof HTMLElement) { @@ -95,16 +98,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 +109,7 @@ export const actionFinalize = register({ !lastCommittedPoint || points[points.length - 1] !== lastCommittedPoint ) { - mutate({ + scene.mutateElement(multiPointElement, { points: multiPointElement.points.slice(0, -1), }); } @@ -140,7 +133,7 @@ export const actionFinalize = register({ if (isLoop) { const linePoints = multiPointElement.points; const firstPoint = linePoints[0]; - mutate({ + scene.mutateElement(multiPointElement, { points: linePoints.map((p, index) => index === linePoints.length - 1 ? pointFrom(firstPoint[0], firstPoint[1]) @@ -160,13 +153,7 @@ export const actionFinalize = register({ -1, arrayToMap(elements), ); - maybeBindLinearElement( - multiPointElement, - appState, - { x, y }, - elementsMap, - elements, - ); + maybeBindLinearElement(multiPointElement, appState, { x, y }, scene); } } @@ -222,7 +209,7 @@ 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, scene) : appState.selectedLinearElement, pendingImageElementId: null, }, diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 5a309b6775..3d2eda0de8 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1670,10 +1670,10 @@ export const actionChangeArrowType = register({ newElement, startHoveredElement, "start", - elementsMap, + app.scene, ); endHoveredElement && - bindLinearElement(newElement, endHoveredElement, "end", elementsMap); + bindLinearElement(newElement, endHoveredElement, "end", app.scene); const startBinding = startElement && newElement.startBinding @@ -1684,7 +1684,6 @@ export const actionChangeArrowType = register({ newElement, startElement, "start", - elementsMap, ), } : null; @@ -1697,7 +1696,6 @@ export const actionChangeArrowType = register({ newElement, endElement, "end", - elementsMap, ), } : null; @@ -1729,7 +1727,7 @@ export const actionChangeArrowType = register({ newElement.startBinding.elementId, ) as ExcalidrawBindableElement; if (startElement) { - bindLinearElement(newElement, startElement, "start", elementsMap); + bindLinearElement(newElement, startElement, "start", app.scene); } } if (newElement.endBinding) { @@ -1737,7 +1735,7 @@ export const actionChangeArrowType = register({ newElement.endBinding.elementId, ) as ExcalidrawBindableElement; if (endElement) { - bindLinearElement(newElement, endElement, "end", elementsMap); + bindLinearElement(newElement, endElement, "end", app.scene); } } } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 0619f6c16d..2eebb32e78 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -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.mutateElement( + frameElement, + { + customData: { generationData: undefined }, + }, + { informMutation: false }, + ); } else { - this.scene.mutateElement(frameElement, { - customData: { generationData: data }, - }, { informMutation: false }); + this.scene.mutateElement( + frameElement, + { + customData: { generationData: data }, + }, + { informMutation: false }, + ); } this.magicGenerations.set(frameElement.id, data); this.triggerRender(); @@ -2926,8 +2934,7 @@ class App extends React.Component { nonDeletedElementsMap, ), ), - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), + this.scene, ); } @@ -3452,7 +3459,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.mutateElement( + initializedImageElement, + { y }, + { informMutation: false }, + ); y = imageElement.y + imageElement.height + 25; @@ -4177,6 +4188,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), this.state, getLinkDirectionFromKey(event.key), + this.scene, ); } @@ -4418,12 +4430,16 @@ class App extends React.Component { } selectedElements.forEach((element) => { - this.scene.mutateElement(element, { - x: element.x + offsetX, - y: element.y + offsetY, - }, { informMutation: false }); + this.scene.mutateElement( + element, + { + x: element.x + offsetX, + y: element.y + offsetY, + }, + { informMutation: false }, + ); - updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { + updateBoundElements(this.scene, element, { simultaneouslyUpdated: selectedElements, }); }); @@ -4457,6 +4473,7 @@ class App extends React.Component { this.setState({ editingLinearElement: new LinearElementEditor( selectedElement, + this.scene, ), }); } @@ -4650,7 +4667,6 @@ 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), @@ -4961,7 +4977,7 @@ class App extends React.Component { onChange: withBatchedUpdates((nextOriginalText) => { updateElement(nextOriginalText, false); if (isNonDeletedElement(element)) { - updateBoundElements(element, this.scene.getNonDeletedElementsMap()); + updateBoundElements(this.scene, element); } }), onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { @@ -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, + ), }); 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, ); @@ -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; @@ -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, + ) : null, showHyperlinkPopup: elementsWithinSelection.length === 1 && @@ -8968,7 +8990,6 @@ class App extends React.Component { element, startBindingElement, endBindingElement, - elementsMap, this.scene, ); } @@ -9109,8 +9130,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 +9148,10 @@ class App extends React.Component { }, prevState, ), - selectedLinearElement: new LinearElementEditor(newElement), + selectedLinearElement: new LinearElementEditor( + newElement, + this.scene, + ), })); } else { this.setState((prevState) => ({ @@ -9268,9 +9291,13 @@ class App extends React.Component { this.state.editingGroupId!, ); - this.scene.mutateElement(element, { - groupIds: element.groupIds.slice(0, index), - }, { informMutation: false }); + this.scene.mutateElement( + element, + { + groupIds: element.groupIds.slice(0, index), + }, + { informMutation: false }, + ); } nextElements.forEach((element) => { @@ -9281,9 +9308,13 @@ class App extends React.Component { element.groupIds[element.groupIds.length - 1], ).length < 2 ) { - this.scene.mutateElement(element, { - groupIds: [], - }, { informMutation: false }); + this.scene.mutateElement( + element, + { + groupIds: [], + }, + { informMutation: false }, + ); } }); @@ -9392,7 +9423,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, + ), }); } } @@ -9517,7 +9551,10 @@ class App extends React.Component { selectedLinearElement: newSelectedElements.length === 1 && isLinearElement(newSelectedElements[0]) - ? new LinearElementEditor(newSelectedElements[0]) + ? new LinearElementEditor( + newSelectedElements[0], + this.scene, + ) : prevState.selectedLinearElement, }; }); @@ -9591,7 +9628,7 @@ 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) : prevState.selectedLinearElement, })); } @@ -9684,7 +9721,6 @@ class App extends React.Component { bindOrUnbindLinearElements( linearElements, - this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), this.scene, isBindingEnabled(this.state), @@ -9845,9 +9881,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.mutateElement( + _imageElement, + { + fileId, + }, + { informMutation: false }, + ) as NonDeleted; return new Promise>( async (resolve, reject) => { @@ -10523,7 +10563,7 @@ class App extends React.Component { this, ), selectedLinearElement: isLinearElement(element) - ? new LinearElementEditor(element) + ? new LinearElementEditor(element, this.scene) : null, } : this.state), @@ -10556,6 +10596,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 +10662,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, @@ -10723,16 +10765,12 @@ class App extends React.Component { ), ); - updateBoundElements( - croppingElement, - this.scene.getNonDeletedElementsMap(), - { - newSize: { - width: croppingElement.width, - height: croppingElement.height, - }, + updateBoundElements(this.scene, croppingElement, { + newSize: { + width: croppingElement.width, + height: croppingElement.height, }, - ); + }); this.setState({ isCropping: transformHandleType && transformHandleType !== "rotation", @@ -10846,7 +10884,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/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index 142abc4074..c51410987f 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType< }; } - mutateElement(element, { + scene.mutateElement(element, { crop: nextCrop, width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), @@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType< height: nextCropHeight, }; - mutateElement(element, { + scene.mutateElement(element, { crop: nextCrop, width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), @@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldMaintainAspectRatio: keepAspectRatio, @@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, + scene, property === "width" ? "e" : "s", { shouldMaintainAspectRatio: keepAspectRatio, diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index 301eb1e796..3c99a9b574 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -244,7 +244,6 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, property === "width" ? "e" : "s", { @@ -347,7 +346,6 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, latestElement, origElement, - elementsMap, originalElementsMap, property === "width" ? "e" : "s", { diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 77fc7e57db..5163564fac 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -85,15 +85,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, ); });