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 = (
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;

View file

@ -84,7 +84,6 @@ import type {
OrderedExcalidrawElement,
ExcalidrawElbowArrowElement,
FixedPoint,
SceneElementsMap,
FixedPointBinding,
} from "./types";
@ -130,7 +129,6 @@ export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep",
endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: NonDeletedSceneElementsMap,
scene: Scene,
): void => {
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = 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<ExcalidrawBindableElement["id"]>,
// Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
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<ExcalidrawLinearElement>[],
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<ExcalidrawLinearElement>,
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<ExcalidrawLinearElement>,
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<ExcalidrawLinearElement>,
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<GlobalPoint>,
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<GlobalPoint>,
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<GlobalPoint>(
bindableElement.x + bindableElement.width / 2,
@ -1349,7 +1320,6 @@ export const calculateFixedPointForElbowArrowBinding = (
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
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: (

View file

@ -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 },
);
}
};

View file

@ -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,
);

View file

@ -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<string, OrderedExcalidrawElement>();
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,
}),
);
}
}

View file

@ -125,13 +125,13 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
constructor(element: NonDeleted<ExcalidrawLinearElement>, 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<ExcalidrawLinearElement>) {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
static normalizePoints(
element: NonDeleted<ExcalidrawLinearElement>,
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<GlobalPoint>(
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,
),
});
}
}

View file

@ -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<ExcalidrawTextElementWithContainer>(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);
}
}

View file

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

View file

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

View file

@ -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),
};
}
}

View file

@ -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<ExcalidrawElbowArrowElement>) =>
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,
},

View file

@ -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);
}
}
}

View file

@ -1950,13 +1950,21 @@ class App extends React.Component<AppProps, AppState> {
// 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<AppProps, AppState> {
nonDeletedElementsMap,
),
),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
);
}
@ -3452,7 +3459,11 @@ class App extends React.Component<AppProps, AppState> {
}
// 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<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
this.state,
getLinkDirectionFromKey(event.key),
this.scene,
);
}
@ -4418,12 +4430,16 @@ class App extends React.Component<AppProps, AppState> {
}
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<AppProps, AppState> {
this.setState({
editingLinearElement: new LinearElementEditor(
selectedElement,
this.scene,
),
});
}
@ -4650,7 +4667,6 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
) {
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<AppProps, AppState> {
this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
this.scene,
midPoint,
);
@ -8113,7 +8132,7 @@ class App extends React.Component<AppProps, AppState> {
index,
gridX,
gridY,
this.scene.getNonDeletedElementsMap(),
this.scene,
);
flushSync(() => {
@ -8218,7 +8237,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords,
this,
!event[KEYS.CTRL_OR_CMD],
elementsMap,
this.scene,
);
if (!ret) {
return;
@ -8800,7 +8819,10 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
element,
startBindingElement,
endBindingElement,
elementsMap,
this.scene,
);
}
@ -9109,8 +9130,7 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
},
prevState,
),
selectedLinearElement: new LinearElementEditor(newElement),
selectedLinearElement: new LinearElementEditor(
newElement,
this.scene,
),
}));
} else {
this.setState((prevState) => ({
@ -9268,9 +9291,13 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
// 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<AppProps, AppState> {
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<AppProps, AppState> {
// 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<AppProps, AppState> {
bindOrUnbindLinearElements(
linearElements,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state),
@ -9845,9 +9881,13 @@ class App extends React.Component<AppProps, AppState> {
const dataURL =
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
const imageElement = this.scene.mutateElement(_imageElement, {
fileId,
}, { informMutation: false }) as NonDeleted<InitializedExcalidrawImageElement>;
const imageElement = this.scene.mutateElement(
_imageElement,
{
fileId,
},
{ informMutation: false },
) as NonDeleted<InitializedExcalidrawImageElement>;
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
async (resolve, reject) => {
@ -10523,7 +10563,7 @@ class App extends React.Component<AppProps, AppState> {
this,
),
selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element)
? new LinearElementEditor(element, this.scene)
: null,
}
: this.state),
@ -10556,6 +10596,7 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
: 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<AppProps, AppState> {
),
);
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<AppProps, AppState> {
pointerDownState.originalElements,
transformHandleType,
selectedElements,
this.scene.getElementsMapIncludingDeleted(),
this.scene,
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),

View file

@ -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,

View file

@ -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",
{

View file

@ -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<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement,
elementsMap,
{} as Scene,
h.app.scene,
);
});