Scene passed in everywhere (WIP)

This commit is contained in:
Marcel Mraz 2025-04-11 00:07:52 +00:00
parent a9c3b2a4d4
commit 9b0a2f86a9
16 changed files with 246 additions and 281 deletions

View file

@ -15,13 +15,12 @@ export interface Alignment {
export const alignElements = ( export const alignElements = (
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment, alignment: Alignment,
scene: Scene, scene: Scene,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const groups: ExcalidrawElement[][] = getMaximumGroups( const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements, selectedElements,
elementsMap, scene.getNonDeletedElementsMap(),
); );
const selectionBoundingBox = getCommonBoundingBox(selectedElements); const selectionBoundingBox = getCommonBoundingBox(selectedElements);
@ -33,12 +32,12 @@ export const alignElements = (
); );
return group.map((element) => { return group.map((element) => {
// update element // update element
const updatedEle = mutateElement(element, { const updatedEle = scene.mutateElement(element, {
x: element.x + translation.x, x: element.x + translation.x,
y: element.y + translation.y, y: element.y + translation.y,
}); });
// update bound elements // update bound elements
updateBoundElements(element, scene.getNonDeletedElementsMap(), { updateBoundElements(scene, element, {
simultaneouslyUpdated: group, simultaneouslyUpdated: group,
}); });
return updatedEle; return updatedEle;

View file

@ -84,7 +84,6 @@ import type {
OrderedExcalidrawElement, OrderedExcalidrawElement,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
FixedPoint, FixedPoint,
SceneElementsMap,
FixedPointBinding, FixedPointBinding,
} from "./types"; } from "./types";
@ -130,7 +129,6 @@ export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep", startBindingElement: ExcalidrawBindableElement | null | "keep",
endBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: NonDeletedSceneElementsMap,
scene: Scene, scene: Scene,
): void => { ): void => {
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@ -142,7 +140,7 @@ export const bindOrUnbindLinearElement = (
"start", "start",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, scene,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@ -151,7 +149,7 @@ export const bindOrUnbindLinearElement = (
"end", "end",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, scene,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@ -159,7 +157,7 @@ export const bindOrUnbindLinearElement = (
); );
getNonDeletedElements(scene, onlyUnbound).forEach((element) => { getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
mutateElement(element, { scene.mutateElement(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
(element) => (element) =>
element.type !== "arrow" || element.id !== linearElement.id, element.type !== "arrow" || element.id !== linearElement.id,
@ -177,7 +175,7 @@ const bindOrUnbindLinearElementEdge = (
boundToElementIds: Set<ExcalidrawBindableElement["id"]>, boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
): void => { ): void => {
// "keep" is for method chaining convenience, a "no-op", so just bail out // "keep" is for method chaining convenience, a "no-op", so just bail out
if (bindableElement === "keep") { if (bindableElement === "keep") {
@ -186,7 +184,7 @@ const bindOrUnbindLinearElementEdge = (
// null means break the bind, so nothing to consider here // null means break the bind, so nothing to consider here
if (bindableElement === null) { if (bindableElement === null) {
const unbound = unbindLinearElement(linearElement, startOrEnd); const unbound = unbindLinearElement(linearElement, scene, startOrEnd);
if (unbound != null) { if (unbound != null) {
unboundFromElementIds.add(unbound); unboundFromElementIds.add(unbound);
} }
@ -209,16 +207,11 @@ const bindOrUnbindLinearElementEdge = (
: startOrEnd === "start" || : startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id) otherEdgeBindableElement.id !== bindableElement.id)
) { ) {
bindLinearElement( bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
linearElement,
bindableElement,
startOrEnd,
elementsMap,
);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
} else { } else {
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap); bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
}; };
@ -362,7 +355,6 @@ const getBindingStrategyForDraggingArrowOrJoints = (
export const bindOrUnbindLinearElements = ( export const bindOrUnbindLinearElements = (
selectedElements: NonDeleted<ExcalidrawLinearElement>[], selectedElements: NonDeleted<ExcalidrawLinearElement>[],
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
scene: Scene, scene: Scene,
isBindingEnabled: boolean, isBindingEnabled: boolean,
@ -376,20 +368,20 @@ export const bindOrUnbindLinearElements = (
selectedElement, selectedElement,
isBindingEnabled, isBindingEnabled,
draggingPoints ?? [], draggingPoints ?? [],
elementsMap, scene.getNonDeletedElementsMap(),
elements, elements,
zoom, zoom,
) )
: // The arrow itself (the shaft) or the inner joins are dragged : // The arrow itself (the shaft) or the inner joins are dragged
getBindingStrategyForDraggingArrowOrJoints( getBindingStrategyForDraggingArrowOrJoints(
selectedElement, selectedElement,
elementsMap, scene.getNonDeletedElementsMap(),
elements, elements,
isBindingEnabled, isBindingEnabled,
zoom, zoom,
); );
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene); bindOrUnbindLinearElement(selectedElement, start, end, scene);
}); });
}; };
@ -429,22 +421,21 @@ export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState, appState: AppState,
pointerCoords: { x: number; y: number }, pointerCoords: { x: number; y: number },
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
elements: readonly NonDeletedExcalidrawElement[],
): void => { ): void => {
if (appState.startBoundElement != null) { if (appState.startBoundElement != null) {
bindLinearElement( bindLinearElement(
linearElement, linearElement,
appState.startBoundElement, appState.startBoundElement,
"start", "start",
elementsMap, scene,
); );
} }
const hoveredElement = getHoveredElementForBinding( const hoveredElement = getHoveredElementForBinding(
pointerCoords, pointerCoords,
elements, scene.getNonDeletedElements(),
elementsMap, scene.getNonDeletedElementsMap(),
appState.zoom, appState.zoom,
isElbowArrow(linearElement), isElbowArrow(linearElement),
isElbowArrow(linearElement), isElbowArrow(linearElement),
@ -458,7 +449,7 @@ export const maybeBindLinearElement = (
"end", "end",
) )
) { ) {
bindLinearElement(linearElement, hoveredElement, "end", elementsMap); bindLinearElement(linearElement, hoveredElement, "end", scene);
} }
} }
}; };
@ -487,7 +478,7 @@ export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
): void => { ): void => {
if (!isArrowElement(linearElement)) { if (!isArrowElement(linearElement)) {
return; return;
@ -500,7 +491,7 @@ export const bindLinearElement = (
linearElement, linearElement,
hoveredElement, hoveredElement,
startOrEnd, startOrEnd,
elementsMap, scene.getNonDeletedElementsMap(),
), ),
hoveredElement, hoveredElement,
), ),
@ -513,18 +504,17 @@ export const bindLinearElement = (
linearElement, linearElement,
hoveredElement, hoveredElement,
startOrEnd, startOrEnd,
elementsMap,
), ),
}; };
} }
mutateElement(linearElement, { scene.mutateElement(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
}); });
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []); const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
if (!boundElementsMap.has(linearElement.id)) { if (!boundElementsMap.has(linearElement.id)) {
mutateElement(hoveredElement, { scene.mutateElement(hoveredElement, {
boundElements: (hoveredElement.boundElements || []).concat({ boundElements: (hoveredElement.boundElements || []).concat({
id: linearElement.id, id: linearElement.id,
type: "arrow", type: "arrow",
@ -565,6 +555,7 @@ const isLinearElementSimple = (
const unbindLinearElement = ( const unbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
scene: Scene,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
): ExcalidrawBindableElement["id"] | null => { ): ExcalidrawBindableElement["id"] | null => {
const field = startOrEnd === "start" ? "startBinding" : "endBinding"; const field = startOrEnd === "start" ? "startBinding" : "endBinding";
@ -572,7 +563,7 @@ const unbindLinearElement = (
if (binding == null) { if (binding == null) {
return null; return null;
} }
mutateElement(linearElement, { [field]: null }); scene.mutateElement(linearElement, { [field]: null });
return binding.elementId; return binding.elementId;
}; };
@ -739,8 +730,8 @@ const calculateFocusAndGap = (
// done before the `changedElement` is updated, and the `newSize` is passed // done before the `changedElement` is updated, and the `newSize` is passed
// in explicitly. // in explicitly.
export const updateBoundElements = ( export const updateBoundElements = (
scene: Scene,
changedElement: NonDeletedExcalidrawElement, changedElement: NonDeletedExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
@ -756,6 +747,7 @@ export const updateBoundElements = (
return; return;
} }
const elementsMap = scene.getNonDeletedElementsMap();
boundElementsVisitor(elementsMap, changedElement, (element) => { boundElementsVisitor(elementsMap, changedElement, (element) => {
if (!isLinearElement(element) || element.isDeleted) { if (!isLinearElement(element) || element.isDeleted) {
return; return;
@ -796,20 +788,7 @@ export const updateBoundElements = (
// `linearElement` is being moved/scaled already, just update the binding // `linearElement` is being moved/scaled already, just update the binding
if (simultaneouslyUpdatedElementIds.has(element.id)) { if (simultaneouslyUpdatedElementIds.has(element.id)) {
if (isElbowArrow(element)) { scene.mutateElement(element, bindings);
mutateElbowArrow(
element,
bindings as {
startBinding: FixedPointBinding;
endBinding: FixedPointBinding;
},
true,
elementsMap,
);
} else {
mutateElement(element, bindings, true);
}
return; return;
} }
@ -898,7 +877,6 @@ export const getHeadingForElbowArrowSnap = (
otherPoint: Readonly<GlobalPoint>, otherPoint: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement | undefined | null, bindableElement: ExcalidrawBindableElement | undefined | null,
aabb: Bounds | undefined | null, aabb: Bounds | undefined | null,
elementsMap: ElementsMap,
origPoint: GlobalPoint, origPoint: GlobalPoint,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
): Heading => { ): Heading => {
@ -908,12 +886,7 @@ export const getHeadingForElbowArrowSnap = (
return otherPointHeading; return otherPointHeading;
} }
const distance = getDistanceForBinding( const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
origPoint,
bindableElement,
elementsMap,
zoom,
);
if (!distance) { if (!distance) {
return vectorToHeading( return vectorToHeading(
@ -933,7 +906,6 @@ export const getHeadingForElbowArrowSnap = (
const getDistanceForBinding = ( const getDistanceForBinding = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement, bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
) => { ) => {
const distance = distanceToBindableElement(bindableElement, point); const distance = distanceToBindableElement(bindableElement, point);
@ -1239,7 +1211,6 @@ const updateBoundPoint = (
linearElement, linearElement,
bindableElement, bindableElement,
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>( const globalMidPoint = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
@ -1349,7 +1320,6 @@ export const calculateFixedPointForElbowArrowBinding = (
linearElement: NonDeleted<ExcalidrawElbowArrowElement>, linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): { fixedPoint: FixedPoint } => { ): { fixedPoint: FixedPoint } => {
const bounds = [ const bounds = [
hoveredElement.x, hoveredElement.x,
@ -2166,6 +2136,7 @@ export class BoundElement {
* NOTE: rebind expects that affected elements were previously unbound with `BoundElement.unbindAffected` * NOTE: rebind expects that affected elements were previously unbound with `BoundElement.unbindAffected`
*/ */
public static rebindAffected = ( public static rebindAffected = (
scene: Scene,
elements: ElementsMap, elements: ElementsMap,
boundElement: ExcalidrawElement | undefined, boundElement: ExcalidrawElement | undefined,
updateElementWith: ( updateElementWith: (

View file

@ -104,7 +104,7 @@ export const dragSelectedElements = (
); );
elementsToUpdate.forEach((element) => { elementsToUpdate.forEach((element) => {
updateElementCoords(pointerDownState, element, adjustedOffset); updateElementCoords(pointerDownState, element, scene, adjustedOffset);
if (!isArrowElement(element)) { if (!isArrowElement(element)) {
// skip arrow labels since we calculate its position during render // skip arrow labels since we calculate its position during render
const textElement = getBoundTextElement( const textElement = getBoundTextElement(
@ -112,9 +112,14 @@ export const dragSelectedElements = (
scene.getNonDeletedElementsMap(), scene.getNonDeletedElementsMap(),
); );
if (textElement) { if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(
pointerDownState,
textElement,
scene,
adjustedOffset,
);
} }
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), { updateBoundElements(scene, element, {
simultaneouslyUpdated: Array.from(elementsToUpdate), simultaneouslyUpdated: Array.from(elementsToUpdate),
}); });
} }
@ -155,6 +160,7 @@ const calculateOffset = (
const updateElementCoords = ( const updateElementCoords = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
scene: Scene,
dragOffset: { x: number; y: number }, dragOffset: { x: number; y: number },
) => { ) => {
const originalElement = const originalElement =
@ -163,7 +169,7 @@ const updateElementCoords = (
const nextX = originalElement.x + dragOffset.x; const nextX = originalElement.x + dragOffset.x;
const nextY = originalElement.y + dragOffset.y; const nextY = originalElement.y + dragOffset.y;
mutateElement(element, { scene.mutateElement(element, {
x: nextX, x: nextX,
y: nextY, y: nextY,
}); });
@ -190,6 +196,7 @@ export const dragNewElement = ({
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
shouldResizeFromCenter, shouldResizeFromCenter,
zoom, zoom,
scene,
widthAspectRatio = null, widthAspectRatio = null,
originOffset = null, originOffset = null,
informMutation = true, informMutation = true,
@ -205,6 +212,7 @@ export const dragNewElement = ({
shouldMaintainAspectRatio: boolean; shouldMaintainAspectRatio: boolean;
shouldResizeFromCenter: boolean; shouldResizeFromCenter: boolean;
zoom: NormalizedZoomValue; zoom: NormalizedZoomValue;
scene: Scene;
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */ true */
widthAspectRatio?: number | null; widthAspectRatio?: number | null;
@ -285,7 +293,7 @@ export const dragNewElement = ({
}; };
} }
mutateElement( scene.mutateElement(
newElement, newElement,
{ {
x: newX + (originOffset?.x ?? 0), x: newX + (originOffset?.x ?? 0),
@ -295,7 +303,7 @@ export const dragNewElement = ({
...textAutoResize, ...textAutoResize,
...imageInitialDimension, ...imageInitialDimension,
}, },
informMutation, { informMutation },
); );
} }
}; };

View file

@ -1329,14 +1329,12 @@ const getElbowArrowData = (
const startHeading = getBindPointHeading( const startHeading = getBindPointHeading(
startGlobalPoint, startGlobalPoint,
endGlobalPoint, endGlobalPoint,
elementsMap,
hoveredStartElement, hoveredStartElement,
origStartGlobalPoint, origStartGlobalPoint,
); );
const endHeading = getBindPointHeading( const endHeading = getBindPointHeading(
endGlobalPoint, endGlobalPoint,
startGlobalPoint, startGlobalPoint,
elementsMap,
hoveredEndElement, hoveredEndElement,
origEndGlobalPoint, origEndGlobalPoint,
); );
@ -2306,7 +2304,6 @@ const getGlobalPoint = (
const getBindPointHeading = ( const getBindPointHeading = (
p: GlobalPoint, p: GlobalPoint,
otherPoint: GlobalPoint, otherPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined, hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: GlobalPoint, origPoint: GlobalPoint,
): Heading => ): Heading =>
@ -2324,7 +2321,6 @@ const getBindPointHeading = (
number, number,
], ],
), ),
elementsMap,
origPoint, origPoint,
); );

View file

@ -7,6 +7,8 @@ import type {
PendingExcalidrawElements, PendingExcalidrawElements,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { bindLinearElement } from "./binding"; import { bindLinearElement } from "./binding";
import { updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import { import {
@ -239,6 +241,7 @@ const addNewNode = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
) => { ) => {
const successors = getSuccessors(element, elementsMap, direction); const successors = getSuccessors(element, elementsMap, direction);
const predeccessors = getPredecessors(element, elementsMap, direction); const predeccessors = getPredecessors(element, elementsMap, direction);
@ -277,6 +280,7 @@ const addNewNode = (
elementsMap, elementsMap,
direction, direction,
appState, appState,
scene,
); );
return { return {
@ -290,6 +294,7 @@ export const addNewNodes = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
numberOfNodes: number, numberOfNodes: number,
) => { ) => {
// always start from 0 and distribute evenly // always start from 0 and distribute evenly
@ -355,6 +360,7 @@ export const addNewNodes = (
elementsMap, elementsMap,
direction, direction,
appState, appState,
scene,
); );
newNodes.push(nextNode); newNodes.push(nextNode);
@ -370,6 +376,7 @@ const createBindingArrow = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
direction: LinkDirection, direction: LinkDirection,
appState: AppState, appState: AppState,
scene: Scene,
) => { ) => {
let startX: number; let startX: number;
let startY: number; let startY: number;
@ -440,18 +447,8 @@ const createBindingArrow = (
elbowed: true, elbowed: true,
}); });
bindLinearElement( bindLinearElement(bindingArrow, startBindingElement, "start", scene);
bindingArrow, bindLinearElement(bindingArrow, endBindingElement, "end", scene);
startBindingElement,
"start",
elementsMap as NonDeletedSceneElementsMap,
);
bindLinearElement(
bindingArrow,
endBindingElement,
"end",
elementsMap as NonDeletedSceneElementsMap,
);
const changedElements = new Map<string, OrderedExcalidrawElement>(); const changedElements = new Map<string, OrderedExcalidrawElement>();
changedElements.set( changedElements.set(
@ -635,6 +632,7 @@ export class FlowChartCreator {
elementsMap: ElementsMap, elementsMap: ElementsMap,
appState: AppState, appState: AppState,
direction: LinkDirection, direction: LinkDirection,
scene: Scene,
) { ) {
if (direction !== this.direction) { if (direction !== this.direction) {
const { nextNode, bindingArrow } = addNewNode( const { nextNode, bindingArrow } = addNewNode(
@ -642,6 +640,7 @@ export class FlowChartCreator {
elementsMap, elementsMap,
appState, appState,
direction, direction,
scene,
); );
this.numberOfNodes = 1; this.numberOfNodes = 1;
@ -655,6 +654,7 @@ export class FlowChartCreator {
elementsMap, elementsMap,
appState, appState,
direction, direction,
scene,
this.numberOfNodes, this.numberOfNodes,
); );
@ -682,13 +682,9 @@ export class FlowChartCreator {
) )
) { ) {
this.pendingNodes = this.pendingNodes.map((node) => this.pendingNodes = this.pendingNodes.map((node) =>
mutateElement( mutateElement(node, {
node, frameId: startNode.frameId,
{ }),
frameId: startNode.frameId,
},
false,
),
); );
} }
} }

View file

@ -125,13 +125,13 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean; public readonly elbowed: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>) { constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
if (!pointsEqual(element.points[0], pointFrom(0, 0))) { if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack); console.error("Linear element is not normalized", Error().stack);
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element, scene);
} }
this.selectedPointsIndices = null; this.selectedPointsIndices = null;
@ -796,7 +796,7 @@ export class LinearElementEditor {
linearElementEditor.lastUncommittedPoint == null && linearElementEditor.lastUncommittedPoint == null &&
!isElbowArrow(element) !isElbowArrow(element)
) { ) {
mutateElement(element, { scene.mutateElement(element, {
points: [ points: [
...element.points, ...element.points,
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
@ -862,7 +862,6 @@ export class LinearElementEditor {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
scene, scene,
); );
} }
@ -1165,19 +1164,23 @@ export class LinearElementEditor {
// element-mutating methods // element-mutating methods
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) { static normalizePoints(
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); element: NonDeleted<ExcalidrawLinearElement>,
scene: Scene,
) {
scene.mutateElement(
element,
LinearElementEditor.getNormalizedPoints(element),
);
} }
static duplicateSelectedPoints( static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
appState: AppState,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): AppState {
invariant( invariant(
appState.editingLinearElement, appState.editingLinearElement,
"Not currently editing a linear element", "Not currently editing a linear element",
); );
const elementsMap = scene.getNonDeletedElementsMap();
const { selectedPointsIndices, elementId } = appState.editingLinearElement; const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
@ -1220,12 +1223,7 @@ export class LinearElementEditor {
return acc; return acc;
}, []); }, []);
const updates = { points: nextPoints }; scene.mutateElement(element, { points: nextPoints });
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
// temp hack to ensure the line doesn't move when adding point to the end, // temp hack to ensure the line doesn't move when adding point to the end,
// potentially expanding the bounding box // potentially expanding the bounding box
@ -1400,8 +1398,9 @@ export class LinearElementEditor {
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
app: AppClassProperties, app: AppClassProperties,
snapToGrid: boolean, snapToGrid: boolean,
elementsMap: ElementsMap, scene: Scene,
) { ) {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap, elementsMap,
@ -1431,12 +1430,7 @@ export class LinearElementEditor {
...element.points.slice(segmentMidpoint.index!), ...element.points.slice(segmentMidpoint.index!),
]; ];
const updates = { points }; scene.mutateElement(element, { points });
if (isElbowArrow(element)) {
mutateElbowArrow(element, updates, true, elementsMap);
} else {
mutateElement(element, updates);
}
ret.pointerDownState = { ret.pointerDownState = {
...linearElementEditor.pointerDownState, ...linearElementEditor.pointerDownState,
@ -1489,7 +1483,7 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints); updates.points = Array.from(nextPoints);
if (!options?.sceneElementsMap) { if (!options?.sceneElementsMap) {
mutateElbowArrow(element, updates, true, options?.sceneElementsMap!, { mutateElbowArrow(element, updates, options?.sceneElementsMap!, {
isDragging: options?.isDragging, isDragging: options?.isDragging,
}); });
} else { } else {
@ -1790,8 +1784,9 @@ export class LinearElementEditor {
index: number, index: number,
x: number, x: number,
y: number, y: number,
elementsMap: ElementsMap, scene: Scene,
): LinearElementEditor { ): LinearElementEditor {
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElement.elementId, linearElement.elementId,
elementsMap, elementsMap,
@ -1834,14 +1829,9 @@ export class LinearElementEditor {
.map((segment) => segment.index) .map((segment) => segment.index)
.reduce((count, idx) => (idx < index ? count + 1 : count), 0); .reduce((count, idx) => (idx < index ? count + 1 : count), 0);
mutateElbowArrow( scene.mutateElement(element, {
element, fixedSegments: nextFixedSegments,
{ });
fixedSegments: nextFixedSegments,
},
true,
elementsMap,
);
const point = pointFrom<GlobalPoint>( const point = pointFrom<GlobalPoint>(
element.x + element.x +
@ -1873,19 +1863,14 @@ export class LinearElementEditor {
static deleteFixedSegment( static deleteFixedSegment(
element: ExcalidrawElbowArrowElement, element: ExcalidrawElbowArrowElement,
elementsMap: NonDeletedSceneElementsMap, scene: Scene,
index: number, index: number,
): void { ): void {
mutateElbowArrow( scene.mutateElement(element, {
element, fixedSegments: element.fixedSegments?.filter(
{ (segment) => segment.index !== index,
fixedSegments: element.fixedSegments?.filter( ),
(segment) => segment.index !== index, });
),
},
true,
elementsMap,
);
} }
} }

View file

@ -85,7 +85,6 @@ export const transformElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene, scene: Scene,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
@ -95,35 +94,35 @@ export const transformElements = (
centerX: number, centerX: number,
centerY: number, centerY: number,
): boolean => { ): boolean => {
const elementsMap = scene.getNonDeletedElementsMap();
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const [element] = selectedElements; const [element] = selectedElements;
if (transformHandleType === "rotation") { if (transformHandleType === "rotation") {
if (!isElbowArrow(element)) { if (!isElbowArrow(element)) {
rotateSingleElement( rotateSingleElement(
element, element,
elementsMap,
scene, scene,
pointerX, pointerX,
pointerY, pointerY,
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
); );
updateBoundElements(element, elementsMap); updateBoundElements(scene, element);
} }
} else if (isTextElement(element) && transformHandleType) { } else if (isTextElement(element) && transformHandleType) {
resizeSingleTextElement( resizeSingleTextElement(
originalElements, originalElements,
element, element,
elementsMap, scene.getNonDeletedElementsMap(),
transformHandleType, transformHandleType,
shouldResizeFromCenter, shouldResizeFromCenter,
pointerX, pointerX,
pointerY, pointerY,
); );
updateBoundElements(element, elementsMap); updateBoundElements(scene, element);
return true; return true;
} else if (transformHandleType) { } else if (transformHandleType) {
const elementId = selectedElements[0].id; const elementId = selectedElements[0].id;
const latestElement = elementsMap.get(elementId); const latestElement = scene.getNonDeletedElementsMap().get(elementId);
const origElement = originalElements.get(elementId); const origElement = originalElements.get(elementId);
if (latestElement && origElement) { if (latestElement && origElement) {
@ -131,7 +130,7 @@ export const transformElements = (
getNextSingleWidthAndHeightFromPointer( getNextSingleWidthAndHeightFromPointer(
latestElement, latestElement,
origElement, origElement,
elementsMap, scene.getNonDeletedElementsMap(),
originalElements, originalElements,
transformHandleType, transformHandleType,
pointerX, pointerX,
@ -147,8 +146,8 @@ export const transformElements = (
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElements, originalElements,
scene,
transformHandleType, transformHandleType,
{ {
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
@ -163,7 +162,6 @@ export const transformElements = (
rotateMultipleElements( rotateMultipleElements(
originalElements, originalElements,
selectedElements, selectedElements,
elementsMap,
scene, scene,
pointerX, pointerX,
pointerY, pointerY,
@ -212,13 +210,15 @@ export const transformElements = (
const rotateSingleElement = ( const rotateSingleElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
scene: Scene, scene: Scene,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
shouldRotateWithDiscreteAngle: boolean, 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 cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
let angle: Radians; let angle: Radians;
@ -235,13 +235,13 @@ const rotateSingleElement = (
} }
const boundTextElementId = getBoundTextElementId(element); const boundTextElementId = getBoundTextElementId(element);
mutateElement(element, { angle }); scene.mutateElement(element, { angle });
if (boundTextElementId) { if (boundTextElementId) {
const textElement = const textElement =
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId); scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
if (textElement && !isArrowElement(element)) { if (textElement && !isArrowElement(element)) {
mutateElement(textElement, { angle }); scene.mutateElement(textElement, { angle });
} }
} }
}; };
@ -517,7 +517,6 @@ const resizeSingleTextElement = (
const rotateMultipleElements = ( const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
elementsMap: SceneElementsMap,
scene: Scene, scene: Scene,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
@ -525,6 +524,7 @@ const rotateMultipleElements = (
centerX: number, centerX: number,
centerY: number, centerY: number,
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
let centerAngle = let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX); (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
if (shouldRotateWithDiscreteAngle) { if (shouldRotateWithDiscreteAngle) {
@ -552,36 +552,27 @@ const rotateMultipleElements = (
{ {
points: getArrowLocalFixedPoints(element, elementsMap), points: getArrowLocalFixedPoints(element, elementsMap),
}, },
false,
elementsMap, elementsMap,
); );
} else { } else {
mutateElement( mutateElement(element, {
element, x: element.x + (rotatedCX - cx),
{ y: element.y + (rotatedCY - cy),
x: element.x + (rotatedCX - cx), angle: normalizeRadians((centerAngle + origAngle) as Radians),
y: element.y + (rotatedCY - cy), });
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
} }
updateBoundElements(element, elementsMap, { updateBoundElements(scene, element, {
simultaneouslyUpdated: elements, simultaneouslyUpdated: elements,
}); });
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) { if (boundText && !isArrowElement(element)) {
mutateElement( mutateElement(boundText, {
boundText, x: boundText.x + (rotatedCX - cx),
{ y: boundText.y + (rotatedCY - cy),
x: boundText.x + (rotatedCX - cx), angle: normalizeRadians((centerAngle + origAngle) as Radians),
y: boundText.y + (rotatedCY - cy), });
angle: normalizeRadians((centerAngle + origAngle) as Radians),
},
false,
);
} }
} }
} }
@ -826,8 +817,8 @@ export const resizeSingleElement = (
nextHeight: number, nextHeight: number,
latestElement: ExcalidrawElement, latestElement: ExcalidrawElement,
origElement: ExcalidrawElement, origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
handleDirection: TransformHandleDirection, handleDirection: TransformHandleDirection,
{ {
shouldInformMutation = true, shouldInformMutation = true,
@ -840,7 +831,10 @@ export const resizeSingleElement = (
} = {}, } = {},
) => { ) => {
let boundTextFont: { fontSize?: number } = {}; let boundTextFont: { fontSize?: number } = {};
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(
latestElement,
scene.getNonDeletedElementsMap(),
);
if (boundTextElement) { if (boundTextElement) {
const stateOfBoundTextElementAtResize = originalElementsMap.get( const stateOfBoundTextElementAtResize = originalElementsMap.get(
@ -860,7 +854,7 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth( const nextFont = measureFontSizeFromWidth(
boundTextElement, boundTextElement,
elementsMap, scene.getNonDeletedElementsMap(),
getBoundTextMaxWidth(updatedElement, boundTextElement), getBoundTextMaxWidth(updatedElement, boundTextElement),
); );
if (nextFont === null) { if (nextFont === null) {
@ -939,7 +933,7 @@ export const resizeSingleElement = (
} }
if ("scale" in latestElement && "scale" in origElement) { if ("scale" in latestElement && "scale" in origElement) {
mutateElement(latestElement, { scene.mutateElement(latestElement, {
scale: [ scale: [
// defaulting because scaleX/Y can be 0/-0 // defaulting because scaleX/Y can be 0/-0
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0], (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
@ -974,21 +968,23 @@ export const resizeSingleElement = (
...rescaledPoints, ...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 // TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight }, newSize: { width: nextWidth, height: nextHeight },
}); });
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { scene.mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize, fontSize: boundTextFont.fontSize,
}); });
} }
handleBindTextResize( handleBindTextResize(
latestElement, latestElement,
elementsMap, scene.getNonDeletedElementsMap(),
handleDirection, handleDirection,
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
); );
@ -1534,26 +1530,22 @@ export const resizeMultipleElements = (
} of elementsAndUpdates) { } of elementsAndUpdates) {
const { width, height, angle } = update; const { width, height, angle } = update;
scene.mutateElement(element, update, false, { scene.mutateElement(element, update, {
// needed for the fixed binding point udpate to take effect // needed for the fixed binding point udpate to take effect
isDragging: true, isDragging: true,
}); });
updateBoundElements(element, elementsMap as SceneElementsMap, { updateBoundElements(scene, element, {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height }, newSize: { width, height },
}); });
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) { if (boundTextElement && boundTextFontSize) {
scene.mutateElement( scene.mutateElement(boundTextElement, {
boundTextElement, fontSize: boundTextFontSize,
{ angle: isLinearElement(element) ? undefined : angle,
fontSize: boundTextFontSize, });
angle: isLinearElement(element) ? undefined : angle,
},
false,
);
handleBindTextResize(element, elementsMap, handleDirection, true); handleBindTextResize(element, elementsMap, handleDirection, true);
} }
} }

View file

@ -50,14 +50,8 @@ const alignSelectedElements = (
alignment: Alignment, alignment: Alignment,
) => { ) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = arrayToMap(elements);
const updatedElements = alignElements( const updatedElements = alignElements(selectedElements, alignment, app.scene);
selectedElements,
elementsMap,
alignment,
app.scene,
);
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);

View file

@ -289,6 +289,7 @@ export const actionDeleteSelected = register({
deleteSelectedElements(elements, appState, app); deleteSelectedElements(elements, appState, app);
fixBindingsAfterDeletion( fixBindingsAfterDeletion(
app.scene,
nextElements, nextElements,
nextElements.filter((el) => el.isDeleted), nextElements.filter((el) => el.isDeleted),
); );

View file

@ -36,6 +36,8 @@ import { CaptureUpdateAction } from "../store";
import { register } from "./register"; import { register } from "./register";
import type Scene from "../scene/Scene";
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
name: "duplicateSelection", name: "duplicateSelection",
label: "labels.duplicateSelection", label: "labels.duplicateSelection",
@ -52,7 +54,7 @@ export const actionDuplicateSelection = register({
try { try {
const newAppState = LinearElementEditor.duplicateSelectedPoints( const newAppState = LinearElementEditor.duplicateSelectedPoints(
appState, appState,
app.scene.getNonDeletedElementsMap(), app.scene,
); );
return { return {
@ -95,7 +97,7 @@ export const actionDuplicateSelection = register({
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)), elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
appState: { appState: {
...appState, ...appState,
...updateLinearElementEditors(duplicatedElements), ...updateLinearElementEditors(duplicatedElements, app.scene),
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
editingGroupId: appState.editingGroupId, 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); const linears = clonedElements.filter(isLinearElement);
if (linears.length === 1) { if (linears.length === 1) {
const linear = linears[0]; const linear = linears[0];
@ -142,7 +147,7 @@ const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
if (onlySingleLinearSelected) { if (onlySingleLinearSelected) {
return { return {
selectedLinearElement: new LinearElementEditor(linear), selectedLinearElement: new LinearElementEditor(linear, scene),
}; };
} }
} }

View file

@ -56,7 +56,6 @@ export const actionFinalize = register({
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
scene, scene,
); );
} }
@ -82,7 +81,11 @@ export const actionFinalize = register({
scene.getElement(appState.pendingImageElementId); scene.getElement(appState.pendingImageElementId);
if (pendingImageElement) { if (pendingImageElement) {
mutateElement(pendingImageElement, { isDeleted: true }, false); scene.mutateElement(
pendingImageElement,
{ isDeleted: true },
{ informMutation: false },
);
} }
if (window.document.activeElement instanceof HTMLElement) { if (window.document.activeElement instanceof HTMLElement) {
@ -95,16 +98,6 @@ export const actionFinalize = register({
? appState.newElement ? appState.newElement
: null; : null;
const mutate = (updates: ElementUpdate<ExcalidrawElbowArrowElement>) =>
isElbowArrow(multiPointElement as ExcalidrawElbowArrowElement)
? mutateElbowArrow(
multiPointElement as ExcalidrawElbowArrowElement,
updates,
true,
elementsMap,
)
: mutateElement(multiPointElement as ExcalidrawElement, updates);
if (multiPointElement) { if (multiPointElement) {
// pen and mouse have hover // pen and mouse have hover
if ( if (
@ -116,7 +109,7 @@ export const actionFinalize = register({
!lastCommittedPoint || !lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint points[points.length - 1] !== lastCommittedPoint
) { ) {
mutate({ scene.mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1), points: multiPointElement.points.slice(0, -1),
}); });
} }
@ -140,7 +133,7 @@ export const actionFinalize = register({
if (isLoop) { if (isLoop) {
const linePoints = multiPointElement.points; const linePoints = multiPointElement.points;
const firstPoint = linePoints[0]; const firstPoint = linePoints[0];
mutate({ scene.mutateElement(multiPointElement, {
points: linePoints.map((p, index) => points: linePoints.map((p, index) =>
index === linePoints.length - 1 index === linePoints.length - 1
? pointFrom(firstPoint[0], firstPoint[1]) ? pointFrom(firstPoint[0], firstPoint[1])
@ -160,13 +153,7 @@ export const actionFinalize = register({
-1, -1,
arrayToMap(elements), arrayToMap(elements),
); );
maybeBindLinearElement( maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
multiPointElement,
appState,
{ x, y },
elementsMap,
elements,
);
} }
} }
@ -222,7 +209,7 @@ export const actionFinalize = register({
// To select the linear element when user has finished mutipoint editing // To select the linear element when user has finished mutipoint editing
selectedLinearElement: selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement) multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement) ? new LinearElementEditor(multiPointElement, scene)
: appState.selectedLinearElement, : appState.selectedLinearElement,
pendingImageElementId: null, pendingImageElementId: null,
}, },

View file

@ -1670,10 +1670,10 @@ export const actionChangeArrowType = register({
newElement, newElement,
startHoveredElement, startHoveredElement,
"start", "start",
elementsMap, app.scene,
); );
endHoveredElement && endHoveredElement &&
bindLinearElement(newElement, endHoveredElement, "end", elementsMap); bindLinearElement(newElement, endHoveredElement, "end", app.scene);
const startBinding = const startBinding =
startElement && newElement.startBinding startElement && newElement.startBinding
@ -1684,7 +1684,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
startElement, startElement,
"start", "start",
elementsMap,
), ),
} }
: null; : null;
@ -1697,7 +1696,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
endElement, endElement,
"end", "end",
elementsMap,
), ),
} }
: null; : null;
@ -1729,7 +1727,7 @@ export const actionChangeArrowType = register({
newElement.startBinding.elementId, newElement.startBinding.elementId,
) as ExcalidrawBindableElement; ) as ExcalidrawBindableElement;
if (startElement) { if (startElement) {
bindLinearElement(newElement, startElement, "start", elementsMap); bindLinearElement(newElement, startElement, "start", app.scene);
} }
} }
if (newElement.endBinding) { if (newElement.endBinding) {
@ -1737,7 +1735,7 @@ export const actionChangeArrowType = register({
newElement.endBinding.elementId, newElement.endBinding.elementId,
) as ExcalidrawBindableElement; ) as ExcalidrawBindableElement;
if (endElement) { if (endElement) {
bindLinearElement(newElement, endElement, "end", elementsMap); bindLinearElement(newElement, endElement, "end", app.scene);
} }
} }
} }

View file

@ -1950,13 +1950,21 @@ class App extends React.Component<AppProps, AppState> {
// state only. // state only.
// Thus reset so that we prefer local cache (if there was some // Thus reset so that we prefer local cache (if there was some
// generationData set previously) // generationData set previously)
this.scene.mutateElement(frameElement, { this.scene.mutateElement(
customData: { generationData: undefined }, frameElement,
}, { informMutation: false }); {
customData: { generationData: undefined },
},
{ informMutation: false },
);
} else { } else {
this.scene.mutateElement(frameElement, { this.scene.mutateElement(
customData: { generationData: data }, frameElement,
}, { informMutation: false }); {
customData: { generationData: data },
},
{ informMutation: false },
);
} }
this.magicGenerations.set(frameElement.id, data); this.magicGenerations.set(frameElement.id, data);
this.triggerRender(); this.triggerRender();
@ -2926,8 +2934,7 @@ class App extends React.Component<AppProps, AppState> {
nonDeletedElementsMap, nonDeletedElementsMap,
), ),
), ),
this.scene.getNonDeletedElementsMap(), this.scene,
this.scene.getNonDeletedElements(),
); );
} }
@ -3452,7 +3459,11 @@ class App extends React.Component<AppProps, AppState> {
} }
// hack to reset the `y` coord because we vertically center during // hack to reset the `y` coord because we vertically center during
// insertImageElement // insertImageElement
this.scene.mutateElement(initializedImageElement, { y }, { informMutation: false }); this.scene.mutateElement(
initializedImageElement,
{ y },
{ informMutation: false },
);
y = imageElement.y + imageElement.height + 25; y = imageElement.y + imageElement.height + 25;
@ -4177,6 +4188,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
getLinkDirectionFromKey(event.key), getLinkDirectionFromKey(event.key),
this.scene,
); );
} }
@ -4418,12 +4430,16 @@ class App extends React.Component<AppProps, AppState> {
} }
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
this.scene.mutateElement(element, { this.scene.mutateElement(
x: element.x + offsetX, element,
y: element.y + offsetY, {
}, { informMutation: false }); x: element.x + offsetX,
y: element.y + offsetY,
},
{ informMutation: false },
);
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { updateBoundElements(this.scene, element, {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}); });
}); });
@ -4457,6 +4473,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor( editingLinearElement: new LinearElementEditor(
selectedElement, selectedElement,
this.scene,
), ),
}); });
} }
@ -4650,7 +4667,6 @@ class App extends React.Component<AppProps, AppState> {
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement), this.scene.getSelectedElements(this.state).filter(isLinearElement),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene, this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
@ -4961,7 +4977,7 @@ class App extends React.Component<AppProps, AppState> {
onChange: withBatchedUpdates((nextOriginalText) => { onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false); updateElement(nextOriginalText, false);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element, this.scene.getNonDeletedElementsMap()); updateBoundElements(this.scene, element);
} }
}), }),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
@ -5454,7 +5470,10 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]), editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene,
),
}); });
return; return;
} else if ( } else if (
@ -5480,7 +5499,7 @@ class App extends React.Component<AppProps, AppState> {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment( LinearElementEditor.deleteFixedSegment(
selectedElements[0], selectedElements[0],
this.scene.getNonDeletedElementsMap(), this.scene,
midPoint, midPoint,
); );
@ -8113,7 +8132,7 @@ class App extends React.Component<AppProps, AppState> {
index, index,
gridX, gridX,
gridY, gridY,
this.scene.getNonDeletedElementsMap(), this.scene,
); );
flushSync(() => { flushSync(() => {
@ -8218,7 +8237,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords, pointerCoords,
this, this,
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
elementsMap, this.scene,
); );
if (!ret) { if (!ret) {
return; return;
@ -8800,7 +8819,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0]) isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor(elementsWithinSelection[0]) ? new LinearElementEditor(
elementsWithinSelection[0],
this.scene,
)
: null, : null,
showHyperlinkPopup: showHyperlinkPopup:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
@ -8968,7 +8990,6 @@ class App extends React.Component<AppProps, AppState> {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
this.scene, this.scene,
); );
} }
@ -9109,8 +9130,7 @@ class App extends React.Component<AppProps, AppState> {
newElement, newElement,
this.state, this.state,
pointerCoords, pointerCoords,
this.scene.getNonDeletedElementsMap(), this.scene,
this.scene.getNonDeletedElements(),
); );
} }
this.setState({ suggestedBindings: [], startBoundElement: null }); this.setState({ suggestedBindings: [], startBoundElement: null });
@ -9128,7 +9148,10 @@ class App extends React.Component<AppProps, AppState> {
}, },
prevState, prevState,
), ),
selectedLinearElement: new LinearElementEditor(newElement), selectedLinearElement: new LinearElementEditor(
newElement,
this.scene,
),
})); }));
} else { } else {
this.setState((prevState) => ({ this.setState((prevState) => ({
@ -9268,9 +9291,13 @@ class App extends React.Component<AppProps, AppState> {
this.state.editingGroupId!, this.state.editingGroupId!,
); );
this.scene.mutateElement(element, { this.scene.mutateElement(
groupIds: element.groupIds.slice(0, index), element,
}, { informMutation: false }); {
groupIds: element.groupIds.slice(0, index),
},
{ informMutation: false },
);
} }
nextElements.forEach((element) => { nextElements.forEach((element) => {
@ -9281,9 +9308,13 @@ class App extends React.Component<AppProps, AppState> {
element.groupIds[element.groupIds.length - 1], element.groupIds[element.groupIds.length - 1],
).length < 2 ).length < 2
) { ) {
this.scene.mutateElement(element, { this.scene.mutateElement(
groupIds: [], element,
}, { informMutation: false }); {
groupIds: [],
},
{ informMutation: false },
);
} }
}); });
@ -9392,7 +9423,10 @@ class App extends React.Component<AppProps, AppState> {
// the one we've hit // the one we've hit
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
this.setState({ this.setState({
selectedLinearElement: new LinearElementEditor(hitElement), selectedLinearElement: new LinearElementEditor(
hitElement,
this.scene,
),
}); });
} }
} }
@ -9517,7 +9551,10 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
newSelectedElements.length === 1 && newSelectedElements.length === 1 &&
isLinearElement(newSelectedElements[0]) isLinearElement(newSelectedElements[0])
? new LinearElementEditor(newSelectedElements[0]) ? new LinearElementEditor(
newSelectedElements[0],
this.scene,
)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
}; };
}); });
@ -9591,7 +9628,7 @@ class App extends React.Component<AppProps, AppState> {
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. // 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 // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement) ? new LinearElementEditor(hitElement, this.scene)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
})); }));
} }
@ -9684,7 +9721,6 @@ class App extends React.Component<AppProps, AppState> {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
linearElements, linearElements,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene, this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
@ -9845,9 +9881,13 @@ class App extends React.Component<AppProps, AppState> {
const dataURL = const dataURL =
this.files[fileId]?.dataURL || (await getDataURL(imageFile)); this.files[fileId]?.dataURL || (await getDataURL(imageFile));
const imageElement = this.scene.mutateElement(_imageElement, { const imageElement = this.scene.mutateElement(
fileId, _imageElement,
}, { informMutation: false }) as NonDeleted<InitializedExcalidrawImageElement>; {
fileId,
},
{ informMutation: false },
) as NonDeleted<InitializedExcalidrawImageElement>;
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>( return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
async (resolve, reject) => { async (resolve, reject) => {
@ -10523,7 +10563,7 @@ class App extends React.Component<AppProps, AppState> {
this, this,
), ),
selectedLinearElement: isLinearElement(element) selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element) ? new LinearElementEditor(element, this.scene)
: null, : null,
} }
: this.state), : this.state),
@ -10556,6 +10596,7 @@ class App extends React.Component<AppProps, AppState> {
height: distance(pointerDownState.origin.y, pointerCoords.y), height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: false, shouldResizeFromCenter: false,
scene: this.scene,
zoom: this.state.zoom.value, zoom: this.state.zoom.value,
informMutation, informMutation,
}); });
@ -10621,6 +10662,7 @@ class App extends React.Component<AppProps, AppState> {
: shouldMaintainAspectRatio(event), : shouldMaintainAspectRatio(event),
shouldResizeFromCenter: shouldResizeFromCenter(event), shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value, zoom: this.state.zoom.value,
scene: this.scene,
widthAspectRatio: aspectRatio, widthAspectRatio: aspectRatio,
originOffset: this.state.originSnapOffset, originOffset: this.state.originSnapOffset,
informMutation, informMutation,
@ -10723,16 +10765,12 @@ class App extends React.Component<AppProps, AppState> {
), ),
); );
updateBoundElements( updateBoundElements(this.scene, croppingElement, {
croppingElement, newSize: {
this.scene.getNonDeletedElementsMap(), width: croppingElement.width,
{ height: croppingElement.height,
newSize: {
width: croppingElement.width,
height: croppingElement.height,
},
}, },
); });
this.setState({ this.setState({
isCropping: transformHandleType && transformHandleType !== "rotation", isCropping: transformHandleType && transformHandleType !== "rotation",
@ -10846,7 +10884,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.originalElements, pointerDownState.originalElements,
transformHandleType, transformHandleType,
selectedElements, selectedElements,
this.scene.getElementsMapIncludingDeleted(),
this.scene, this.scene,
shouldRotateWithDiscreteAngle(event), shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),

View file

@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
}; };
} }
mutateElement(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
height: nextCropHeight, height: nextCropHeight,
}; };
mutateElement(element, { scene.mutateElement(element, {
crop: nextCrop, crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldMaintainAspectRatio: keepAspectRatio, shouldMaintainAspectRatio: keepAspectRatio,
@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
scene,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
shouldMaintainAspectRatio: keepAspectRatio, shouldMaintainAspectRatio: keepAspectRatio,

View file

@ -244,7 +244,6 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {
@ -347,7 +346,6 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
latestElement, latestElement,
origElement, origElement,
elementsMap,
originalElementsMap, originalElementsMap,
property === "width" ? "e" : "s", property === "width" ? "e" : "s",
{ {

View file

@ -85,15 +85,13 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
const elementsMap = h.app.scene.getNonDeletedElementsMap();
act(() => { act(() => {
// bind line to two rectangles // bind line to two rectangles
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
arrow.get() as NonDeleted<ExcalidrawLinearElement>, arrow.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement, rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement,
elementsMap, h.app.scene,
{} as Scene,
); );
}); });