chore: Refactor and remove scene from elbow arrow generation (#8342)

* Refactor and remove scene from elbow arrow generation
This commit is contained in:
Márk Tolmács 2024-08-08 14:06:26 +02:00 committed by GitHub
parent 72d6ee48fc
commit dd1370381d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 115 additions and 156 deletions

View file

@ -25,6 +25,7 @@ const deleteSelectedElements = (
appState: AppState, appState: AppState,
app: AppClassProperties, app: AppClassProperties,
) => { ) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
const framesToBeDeleted = new Set( const framesToBeDeleted = new Set(
getSelectedElements( getSelectedElements(
elements.filter((el) => isFrameLikeElement(el)), elements.filter((el) => isFrameLikeElement(el)),
@ -51,7 +52,7 @@ const deleteSelectedElements = (
? null ? null
: bound.endBinding, : bound.endBinding,
}); });
mutateElbowArrow(bound, app.scene, bound.points); mutateElbowArrow(bound, elementsMap, bound.points);
} }
}); });
} }
@ -159,7 +160,7 @@ export const actionDeleteSelected = register({
LinearElementEditor.deletePoints( LinearElementEditor.deletePoints(
element, element,
selectedPointsIndices, selectedPointsIndices,
app.scene, elementsMap,
); );
return { return {

View file

@ -44,7 +44,7 @@ export const actionDuplicateSelection = register({
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const ret = LinearElementEditor.duplicateSelectedPoints( const ret = LinearElementEditor.duplicateSelectedPoints(
appState, appState,
app.scene, app.scene.getNonDeletedElementsMap(),
); );
if (!ret) { if (!ret) {

View file

@ -120,7 +120,6 @@ const flipElements = (
true, true,
flipDirection === "horizontal" ? maxX : minX, flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY, flipDirection === "horizontal" ? minY : maxY,
app.scene,
); );
bindOrUnbindLinearElements( bindOrUnbindLinearElements(

View file

@ -58,7 +58,6 @@ export const createUndoAction: ActionCreator = (history, store) => ({
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap` arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState, appState,
store.snapshot, store.snapshot,
app.scene,
), ),
), ),
keyTest: (event) => keyTest: (event) =>
@ -100,7 +99,6 @@ export const createRedoAction: ActionCreator = (history, store) => ({
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap` arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState, appState,
store.snapshot, store.snapshot,
app.scene,
), ),
), ),
keyTest: (event) => keyTest: (event) =>

View file

@ -1648,7 +1648,7 @@ export const actionChangeArrowType = register({
mutateElbowArrow( mutateElbowArrow(
newElement, newElement,
app.scene, elementsMap,
[finalStartPoint, finalEndPoint].map( [finalStartPoint, finalEndPoint].map(
(point) => (point) =>
[point[0] - newElement.x, point[1] - newElement.y] as Point, [point[0] - newElement.x, point[1] - newElement.y] as Point,

View file

@ -29,7 +29,6 @@ import type {
} from "./element/types"; } from "./element/types";
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex"; import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups"; import { getNonDeletedGroupIds } from "./groups";
import type Scene from "./scene/Scene";
import { getObservedAppState } from "./store"; import { getObservedAppState } from "./store";
import type { import type {
AppState, AppState,
@ -1054,7 +1053,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
public applyTo( public applyTo(
elements: SceneElementsMap, elements: SceneElementsMap,
snapshot: Map<string, OrderedExcalidrawElement>, snapshot: Map<string, OrderedExcalidrawElement>,
scene: Scene,
): [SceneElementsMap, boolean] { ): [SceneElementsMap, boolean] {
let nextElements = toBrandedType<SceneElementsMap>(new Map(elements)); let nextElements = toBrandedType<SceneElementsMap>(new Map(elements));
let changedElements: Map<string, OrderedExcalidrawElement>; let changedElements: Map<string, OrderedExcalidrawElement>;
@ -1102,7 +1100,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
try { try {
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state // TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements); ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
ElementsChange.redrawBoundArrows(nextElements, changedElements, scene);
// the following reorder performs also mutations, but only on new instances of changed elements // the following reorder performs also mutations, but only on new instances of changed elements
// (unless something goes really bad and it fallbacks to fixing all invalid indices) // (unless something goes really bad and it fallbacks to fixing all invalid indices)
@ -1111,6 +1108,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
changedElements, changedElements,
flags, flags,
); );
// Need ordered nextElements to avoid z-index binding issues
ElementsChange.redrawBoundArrows(nextElements, changedElements);
} catch (e) { } catch (e) {
console.error( console.error(
`Couldn't mutate elements after applying elements change`, `Couldn't mutate elements after applying elements change`,
@ -1459,11 +1459,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
private static redrawBoundArrows( private static redrawBoundArrows(
elements: SceneElementsMap, elements: SceneElementsMap,
changed: Map<string, OrderedExcalidrawElement>, changed: Map<string, OrderedExcalidrawElement>,
scene: Scene,
) { ) {
for (const element of changed.values()) { for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) { if (!element.isDeleted && isBindableElement(element)) {
updateBoundElements(element, elements, scene, { updateBoundElements(element, elements, {
changedElements: changed, changedElements: changed,
}); });
} }

View file

@ -4005,14 +4005,9 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + offsetY, y: element.y + offsetY,
}); });
updateBoundElements( updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
element,
this.scene.getNonDeletedElementsMap(),
this.scene,
{
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}, });
);
}); });
this.setState({ this.setState({
@ -4469,7 +4464,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, elementsMap, this.scene); updateBoundElements(element, this.scene.getNonDeletedElementsMap());
} }
}), }),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
@ -5279,7 +5274,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
this.state, this.state,
this.scene, this.scene.getNonDeletedElementsMap(),
); );
if ( if (
@ -5395,7 +5390,7 @@ class App extends React.Component<AppProps, AppState> {
if (isElbowArrow(multiElement)) { if (isElbowArrow(multiElement)) {
mutateElbowArrow( mutateElbowArrow(
multiElement, multiElement,
this.scene, this.scene.getNonDeletedElementsMap(),
[ [
...points.slice(0, -1), ...points.slice(0, -1),
[ [
@ -7771,7 +7766,7 @@ class App extends React.Component<AppProps, AppState> {
} else if (points.length > 1 && isElbowArrow(newElement)) { } else if (points.length > 1 && isElbowArrow(newElement)) {
mutateElbowArrow( mutateElbowArrow(
newElement, newElement,
this.scene, elementsMap,
[...points.slice(0, -1), [dx, dy]], [...points.slice(0, -1), [dx, dy]],
[0, 0], [0, 0],
undefined, undefined,
@ -9756,7 +9751,6 @@ class App extends React.Component<AppProps, AppState> {
resizeY, resizeY,
pointerDownState.resize.center.x, pointerDownState.resize.center.x,
pointerDownState.resize.center.y, pointerDownState.resize.center.y,
this.scene,
) )
) { ) {
const suggestedBindings = getSuggestedBindingsForArrows( const suggestedBindings = getSuggestedBindingsForArrows(

View file

@ -66,7 +66,6 @@ const resizeElementInGroup = (
origElement: ExcalidrawElement, origElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
) => { ) => {
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement); const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
const { width: oldWidth, height: oldHeight } = latestElement; const { width: oldWidth, height: oldHeight } = latestElement;
@ -78,7 +77,7 @@ const resizeElementInGroup = (
); );
if (boundTextElement) { if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale; const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, scene, { updateBoundElements(latestElement, elementsMap, {
oldSize: { width: oldWidth, height: oldHeight }, oldSize: { width: oldWidth, height: oldHeight },
}); });
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
@ -111,7 +110,6 @@ const resizeGroup = (
originalElements: ExcalidrawElement[], originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
) => { ) => {
// keep aspect ratio for groups // keep aspect ratio for groups
if (property === "width") { if (property === "width") {
@ -135,7 +133,6 @@ const resizeGroup = (
origElement, origElement,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} }
}; };
@ -190,7 +187,6 @@ const handleDimensionChange: DragInputCallbackType<
originalElements, originalElements,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} else { } else {
const [el] = elementsInUnit; const [el] = elementsInUnit;
@ -296,7 +292,6 @@ const handleDimensionChange: DragInputCallbackType<
originalElements, originalElements,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} else { } else {
const [el] = elementsInUnit; const [el] = elementsInUnit;

View file

@ -198,7 +198,7 @@ export const resizeElement = (
} }
} }
updateBoundElements(latestElement, elementsMap, scene, { updateBoundElements(latestElement, elementsMap, {
oldSize: { width: oldWidth, height: oldHeight }, oldSize: { width: oldWidth, height: oldHeight },
}); });
@ -316,6 +316,6 @@ export const updateBindings = (
[], [],
); );
} else { } else {
updateBoundElements(latestElement, elementsMap, scene, options); updateBoundElements(latestElement, elementsMap, options);
} }
}; };

View file

@ -25,6 +25,7 @@ import type {
OrderedExcalidrawElement, OrderedExcalidrawElement,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
FixedPoint, FixedPoint,
SceneElementsMap,
} from "./types"; } from "./types";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
@ -124,7 +125,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, elementsMap,
scene,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@ -134,7 +134,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, elementsMap,
scene,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@ -161,7 +160,6 @@ const bindOrUnbindLinearElementEdge = (
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap, 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") {
@ -571,8 +569,7 @@ const calculateFocusAndGap = (
// in explicitly. // in explicitly.
export const updateBoundElements = ( export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement, changedElement: NonDeletedExcalidrawElement,
elementsMap: ElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
scene: Scene,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
oldSize?: { width: number; height: number }; oldSize?: { width: number; height: number };
@ -658,7 +655,7 @@ export const updateBoundElements = (
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
updates, updates,
scene, elementsMap,
{ {
...(changedElement.id === element.startBinding?.elementId ...(changedElement.id === element.startBinding?.elementId
? { startBinding: bindings.startBinding } ? { startBinding: bindings.startBinding }

View file

@ -91,14 +91,9 @@ export const dragSelectedElements = (
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(pointerDownState, textElement, adjustedOffset);
} }
} }
updateBoundElements( updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
element,
scene.getElementsMapIncludingDeleted(),
scene,
{
simultaneouslyUpdated: Array.from(elementsToUpdate), simultaneouslyUpdated: Array.from(elementsToUpdate),
}, });
);
}); });
}; };

View file

@ -9,6 +9,7 @@ import type {
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
OrderedExcalidrawElement, OrderedExcalidrawElement,
FixedPointBinding, FixedPointBinding,
SceneElementsMap,
} from "./types"; } from "./types";
import { import {
distance2d, distance2d,
@ -290,7 +291,7 @@ export class LinearElementEditor {
isDragging: selectedIndex === lastClickedPoint, isDragging: selectedIndex === lastClickedPoint,
}, },
], ],
scene, elementsMap,
); );
} else { } else {
const newDraggingPointPosition = LinearElementEditor.createPointAt( const newDraggingPointPosition = LinearElementEditor.createPointAt(
@ -326,7 +327,7 @@ export class LinearElementEditor {
isDragging: pointIndex === lastClickedPoint, isDragging: pointIndex === lastClickedPoint,
}; };
}), }),
scene, elementsMap,
); );
} }
@ -420,7 +421,7 @@ export class LinearElementEditor {
: element.points[0], : element.points[0],
}, },
], ],
scene, elementsMap,
); );
} }
@ -876,13 +877,12 @@ export class LinearElementEditor {
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
appState: AppState, appState: AppState,
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): LinearElementEditor | null { ): LinearElementEditor | null {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return null; return null;
} }
const { elementId, lastUncommittedPoint } = appState.editingLinearElement; const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return appState.editingLinearElement; return appState.editingLinearElement;
@ -893,7 +893,11 @@ export class LinearElementEditor {
if (!event.altKey) { if (!event.altKey) {
if (lastPoint === lastUncommittedPoint) { if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, [points.length - 1], scene); LinearElementEditor.deletePoints(
element,
[points.length - 1],
elementsMap,
);
} }
return { return {
...appState.editingLinearElement, ...appState.editingLinearElement,
@ -939,14 +943,13 @@ export class LinearElementEditor {
point: newPoint, point: newPoint,
}, },
], ],
scene, elementsMap,
); );
} else { } else {
LinearElementEditor.addPoints( LinearElementEditor.addPoints(
element, element,
appState,
[{ point: newPoint }], [{ point: newPoint }],
scene, elementsMap,
); );
} }
return { return {
@ -1091,7 +1094,7 @@ export class LinearElementEditor {
const offsetY = points[0][1]; const offsetY = points[0][1];
return { return {
points: points.map((point, _idx) => { points: points.map((point) => {
return [point[0] - offsetX, point[1] - offsetY] as const; return [point[0] - offsetX, point[1] - offsetY] as const;
}), }),
x: element.x + offsetX, x: element.x + offsetX,
@ -1106,13 +1109,15 @@ export class LinearElementEditor {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
} }
static duplicateSelectedPoints(appState: AppState, scene: Scene) { static duplicateSelectedPoints(
appState: AppState,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return false; return false;
} }
const { selectedPointsIndices, elementId } = appState.editingLinearElement; const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element || selectedPointsIndices === null) { if (!element || selectedPointsIndices === null) {
@ -1163,7 +1168,7 @@ export class LinearElementEditor {
point: [lastPoint[0] + 30, lastPoint[1] + 30], point: [lastPoint[0] + 30, lastPoint[1] + 30],
}, },
], ],
scene, elementsMap,
); );
} }
@ -1181,7 +1186,7 @@ export class LinearElementEditor {
static deletePoints( static deletePoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointIndices: readonly number[], pointIndices: readonly number[],
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) { ) {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
@ -1214,15 +1219,14 @@ export class LinearElementEditor {
nextPoints, nextPoints,
offsetX, offsetX,
offsetY, offsetY,
scene, elementsMap,
); );
} }
static addPoints( static addPoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
appState: AppState,
targetPoints: { point: Point }[], targetPoints: { point: Point }[],
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) { ) {
const offsetX = 0; const offsetX = 0;
const offsetY = 0; const offsetY = 0;
@ -1233,14 +1237,14 @@ export class LinearElementEditor {
nextPoints, nextPoints,
offsetX, offsetX,
offsetY, offsetY,
scene, elementsMap,
); );
} }
static movePoints( static movePoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { index: number; point: Point; isDragging?: boolean }[], targetPoints: { index: number; point: Point; isDragging?: boolean }[],
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
otherUpdates?: { otherUpdates?: {
startBinding?: PointBinding | null; startBinding?: PointBinding | null;
endBinding?: PointBinding | null; endBinding?: PointBinding | null;
@ -1296,7 +1300,7 @@ export class LinearElementEditor {
nextPoints, nextPoints,
offsetX, offsetX,
offsetY, offsetY,
scene, elementsMap,
otherUpdates, otherUpdates,
{ {
isDragging: targetPoints.reduce( isDragging: targetPoints.reduce(
@ -1413,7 +1417,7 @@ export class LinearElementEditor {
nextPoints: readonly Point[], nextPoints: readonly Point[],
offsetX: number, offsetX: number,
offsetY: number, offsetY: number,
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
otherUpdates?: { otherUpdates?: {
startBinding?: PointBinding | null; startBinding?: PointBinding | null;
endBinding?: PointBinding | null; endBinding?: PointBinding | null;
@ -1445,7 +1449,7 @@ export class LinearElementEditor {
mutateElbowArrow( mutateElbowArrow(
element, element,
scene, elementsMap,
nextPoints, nextPoints,
[offsetX, offsetY], [offsetX, offsetY],
bindings, bindings,

View file

@ -11,6 +11,8 @@ import type {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawImageElement, ExcalidrawImageElement,
ElementsMap, ElementsMap,
NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types"; } from "./types";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
import { import {
@ -69,7 +71,7 @@ export const transformElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap, elementsMap: SceneElementsMap,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean, shouldMaintainAspectRatio: boolean,
@ -77,7 +79,6 @@ export const transformElements = (
pointerY: number, pointerY: number,
centerX: number, centerX: number,
centerY: number, centerY: number,
scene: Scene,
) => { ) => {
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const [element] = selectedElements; const [element] = selectedElements;
@ -90,7 +91,7 @@ export const transformElements = (
pointerY, pointerY,
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
); );
updateBoundElements(element, elementsMap, scene); updateBoundElements(element, elementsMap);
} }
} else if (isTextElement(element) && transformHandleType) { } else if (isTextElement(element) && transformHandleType) {
resizeSingleTextElement( resizeSingleTextElement(
@ -102,7 +103,7 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
updateBoundElements(element, elementsMap, scene); updateBoundElements(element, elementsMap);
} else if (transformHandleType) { } else if (transformHandleType) {
resizeSingleElement( resizeSingleElement(
originalElements, originalElements,
@ -113,7 +114,6 @@ export const transformElements = (
shouldResizeFromCenter, shouldResizeFromCenter,
pointerX, pointerX,
pointerY, pointerY,
scene,
); );
} }
@ -129,7 +129,6 @@ export const transformElements = (
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
centerX, centerX,
centerY, centerY,
scene,
); );
return true; return true;
} else if (transformHandleType) { } else if (transformHandleType) {
@ -142,7 +141,6 @@ export const transformElements = (
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
pointerX, pointerX,
pointerY, pointerY,
scene,
); );
return true; return true;
} }
@ -434,12 +432,11 @@ export const resizeSingleElement = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean, shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap, elementsMap: SceneElementsMap,
transformHandleDirection: TransformHandleDirection, transformHandleDirection: TransformHandleDirection,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
scene: Scene,
) => { ) => {
const stateAtResizeStart = originalElements.get(element.id)!; const stateAtResizeStart = originalElements.get(element.id)!;
// Gets bounds corners // Gets bounds corners
@ -710,7 +707,7 @@ export const resizeSingleElement = (
) { ) {
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
updateBoundElements(element, elementsMap, scene, { updateBoundElements(element, elementsMap, {
oldSize: { oldSize: {
width: stateAtResizeStart.width, width: stateAtResizeStart.width,
height: stateAtResizeStart.height, height: stateAtResizeStart.height,
@ -734,13 +731,12 @@ export const resizeSingleElement = (
export const resizeMultipleElements = ( export const resizeMultipleElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
transformHandleType: TransformHandleDirection, transformHandleType: TransformHandleDirection,
shouldResizeFromCenter: boolean, shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean, shouldMaintainAspectRatio: boolean,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
scene: Scene,
) => { ) => {
// map selected elements to the original elements. While it never should // map selected elements to the original elements. While it never should
// happen that pointerDownState.originalElements won't contain the selected // happen that pointerDownState.originalElements won't contain the selected
@ -974,12 +970,19 @@ export const resizeMultipleElements = (
mutateElement(element, update, false); mutateElement(element, update, false);
if (isArrowElement(element) && isElbowArrow(element)) { if (isArrowElement(element) && isElbowArrow(element)) {
mutateElbowArrow(element, scene, element.points, undefined, undefined, { mutateElbowArrow(
element,
elementsMap,
element.points,
undefined,
undefined,
{
informMutation: false, informMutation: false,
}); },
);
} }
updateBoundElements(element, elementsMap, scene, { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
oldSize: { width: oldWidth, height: oldHeight }, oldSize: { width: oldWidth, height: oldHeight },
}); });
@ -1004,13 +1007,12 @@ export const resizeMultipleElements = (
const rotateMultipleElements = ( const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"], originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap, elementsMap: SceneElementsMap,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
centerX: number, centerX: number,
centerY: number, centerY: number,
scene: Scene,
) => { ) => {
let centerAngle = let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX); (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
@ -1037,7 +1039,7 @@ const rotateMultipleElements = (
if (isArrowElement(element) && isElbowArrow(element)) { if (isArrowElement(element) && isElbowArrow(element)) {
const points = getArrowLocalFixedPoints(element, elementsMap); const points = getArrowLocalFixedPoints(element, elementsMap);
mutateElbowArrow(element, scene, points); mutateElbowArrow(element, elementsMap, points);
} else { } else {
mutateElement( mutateElement(
element, element,
@ -1050,7 +1052,7 @@ const rotateMultipleElements = (
); );
} }
updateBoundElements(element, elementsMap, scene, { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements, simultaneouslyUpdated: elements,
}); });

View file

@ -45,7 +45,7 @@ describe("elbow arrow routing", () => {
elbowed: true, elbowed: true,
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow); scene.insertElement(arrow);
mutateElbowArrow(arrow, scene, [ mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
[-45 - arrow.x, -100.1 - arrow.y], [-45 - arrow.x, -100.1 - arrow.y],
[45 - arrow.x, 99.9 - arrow.y], [45 - arrow.x, 99.9 - arrow.y],
]); ]);
@ -98,7 +98,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);
mutateElbowArrow(arrow, scene, [ mutateElbowArrow(arrow, elementsMap, [
[0, 0], [0, 0],
[90, 200], [90, 200],
]); ]);

View file

@ -10,7 +10,6 @@ import {
translatePoint, translatePoint,
} from "../math"; } from "../math";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import type Scene from "../scene/Scene";
import type { Point } from "../types"; import type { Point } from "../types";
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils"; import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
import { import {
@ -37,14 +36,10 @@ import { isBindableElement, isRectanguloidElement } from "./typeChecks";
import type { import type {
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
FixedPointBinding, FixedPointBinding,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types"; } from "./types";
import type { import type { ElementsMap, ExcalidrawBindableElement } from "./types";
ElementsMap,
ExcalidrawBindableElement,
OrderedExcalidrawElement,
} from "./types";
type Node = { type Node = {
f: number; f: number;
@ -67,7 +62,7 @@ const BASE_PADDING = 40;
export const mutateElbowArrow = ( export const mutateElbowArrow = (
arrow: ExcalidrawElbowArrowElement, arrow: ExcalidrawElbowArrowElement,
scene: Scene, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
nextPoints: readonly Point[], nextPoints: readonly Point[],
offset?: Point, offset?: Point,
otherUpdates?: { otherUpdates?: {
@ -75,15 +70,11 @@ export const mutateElbowArrow = (
endBinding?: FixedPointBinding | null; endBinding?: FixedPointBinding | null;
}, },
options?: { options?: {
changedElements?: Map<string, OrderedExcalidrawElement>;
isDragging?: boolean; isDragging?: boolean;
disableBinding?: boolean; disableBinding?: boolean;
informMutation?: boolean; informMutation?: boolean;
}, },
) => { ) => {
const elements = getAllElements(scene, options?.changedElements);
const elementsMap = getAllElementsMap(scene, options?.changedElements);
const origStartGlobalPoint = translatePoint(nextPoints[0], [ const origStartGlobalPoint = translatePoint(nextPoints[0], [
arrow.x + (offset ? offset[0] : 0), arrow.x + (offset ? offset[0] : 0),
arrow.y + (offset ? offset[1] : 0), arrow.y + (offset ? offset[1] : 0),
@ -99,22 +90,9 @@ export const mutateElbowArrow = (
const endElement = const endElement =
arrow.endBinding && arrow.endBinding &&
getBindableElementForId(arrow.endBinding.elementId, elementsMap); getBindableElementForId(arrow.endBinding.elementId, elementsMap);
const hoveredStartElement = options?.isDragging const [hoveredStartElement, hoveredEndElement] = options?.isDragging
? getHoveredElementForBinding( ? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap)
tupleToCoors(origStartGlobalPoint), : [startElement, endElement];
elements,
elementsMap,
true,
)
: startElement;
const hoveredEndElement = options?.isDragging
? getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint),
elements,
elementsMap,
true,
)
: endElement;
const startGlobalPoint = getGlobalPoint( const startGlobalPoint = getGlobalPoint(
arrow.startBinding?.fixedPoint, arrow.startBinding?.fixedPoint,
origStartGlobalPoint, origStartGlobalPoint,
@ -895,7 +873,7 @@ const normalizedArrowElementUpdate = (
const offsetY = global[0][1]; const offsetY = global[0][1];
const points = global.map( const points = global.map(
(point, _idx) => [point[0] - offsetX, point[1] - offsetY] as const, (point) => [point[0] - offsetX, point[1] - offsetY] as const,
); );
return { return {
@ -935,32 +913,11 @@ const neighborIndexToHeading = (idx: number): Heading => {
return HEADING_LEFT; return HEADING_LEFT;
}; };
const getAllElementsMap = (
scene: Scene,
changedElements?: Map<string, OrderedExcalidrawElement>,
): NonDeletedSceneElementsMap =>
changedElements
? toBrandedType<NonDeletedSceneElementsMap>(
new Map([...scene.getNonDeletedElementsMap(), ...changedElements]),
)
: scene.getNonDeletedElementsMap();
const getAllElements = (
scene: Scene,
changedElements?: Map<string, OrderedExcalidrawElement>,
): readonly NonDeletedExcalidrawElement[] =>
changedElements
? ([
...scene.getNonDeletedElements(),
...[...changedElements].map(([_, value]) => value),
] as NonDeletedExcalidrawElement[])
: scene.getNonDeletedElements();
const getGlobalPoint = ( const getGlobalPoint = (
fixedPointRatio: [number, number] | undefined | null, fixedPointRatio: [number, number] | undefined | null,
initialPoint: Point, initialPoint: Point,
otherPoint: Point, otherPoint: Point,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
boundElement?: ExcalidrawBindableElement | null, boundElement?: ExcalidrawBindableElement | null,
hoveredElement?: ExcalidrawBindableElement | null, hoveredElement?: ExcalidrawBindableElement | null,
isDragging?: boolean, isDragging?: boolean,
@ -1016,7 +973,7 @@ const getSnapPoint = (
const getBindPointHeading = ( const getBindPointHeading = (
point: Point, point: Point,
otherPoint: Point, otherPoint: Point,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined, hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: Point, origPoint: Point,
) => ) =>
@ -1034,3 +991,30 @@ const getBindPointHeading = (
elementsMap, elementsMap,
origPoint, origPoint,
); );
const getHoveredElements = (
origStartGlobalPoint: Point,
origEndGlobalPoint: Point,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) => {
// TODO: Might be a performance bottleneck and the Map type
// remembers the insertion order anyway...
const nonDeletedSceneElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
new Map([...elementsMap].filter((el) => !el[1].isDeleted)),
);
const elements = Array.from(elementsMap.values());
return [
getHoveredElementForBinding(
tupleToCoors(origStartGlobalPoint),
elements,
nonDeletedSceneElementsMap,
true,
),
getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint),
elements,
nonDeletedSceneElementsMap,
true,
),
];
};

View file

@ -1,7 +1,6 @@
import type { AppStateChange, ElementsChange } from "./change"; import type { AppStateChange, ElementsChange } from "./change";
import type { SceneElementsMap } from "./element/types"; import type { SceneElementsMap } from "./element/types";
import { Emitter } from "./emitter"; import { Emitter } from "./emitter";
import type Scene from "./scene/Scene";
import type { Snapshot } from "./store"; import type { Snapshot } from "./store";
import type { AppState } from "./types"; import type { AppState } from "./types";
@ -65,7 +64,6 @@ export class History {
elements: SceneElementsMap, elements: SceneElementsMap,
appState: AppState, appState: AppState,
snapshot: Readonly<Snapshot>, snapshot: Readonly<Snapshot>,
scene: Scene,
) { ) {
return this.perform( return this.perform(
elements, elements,
@ -73,7 +71,6 @@ export class History {
snapshot, snapshot,
() => History.pop(this.undoStack), () => History.pop(this.undoStack),
(entry: HistoryEntry) => History.push(this.redoStack, entry, elements), (entry: HistoryEntry) => History.push(this.redoStack, entry, elements),
scene,
); );
} }
@ -81,7 +78,6 @@ export class History {
elements: SceneElementsMap, elements: SceneElementsMap,
appState: AppState, appState: AppState,
snapshot: Readonly<Snapshot>, snapshot: Readonly<Snapshot>,
scene: Scene,
) { ) {
return this.perform( return this.perform(
elements, elements,
@ -89,7 +85,6 @@ export class History {
snapshot, snapshot,
() => History.pop(this.redoStack), () => History.pop(this.redoStack),
(entry: HistoryEntry) => History.push(this.undoStack, entry, elements), (entry: HistoryEntry) => History.push(this.undoStack, entry, elements),
scene,
); );
} }
@ -99,7 +94,6 @@ export class History {
snapshot: Readonly<Snapshot>, snapshot: Readonly<Snapshot>,
pop: () => HistoryEntry | null, pop: () => HistoryEntry | null,
push: (entry: HistoryEntry) => void, push: (entry: HistoryEntry) => void,
scene: Scene,
): [SceneElementsMap, AppState] | void { ): [SceneElementsMap, AppState] | void {
try { try {
let historyEntry = pop(); let historyEntry = pop();
@ -116,7 +110,7 @@ export class History {
while (historyEntry) { while (historyEntry) {
try { try {
[nextElements, nextAppState, containsVisibleChange] = [nextElements, nextAppState, containsVisibleChange] =
historyEntry.applyTo(nextElements, nextAppState, snapshot, scene); historyEntry.applyTo(nextElements, nextAppState, snapshot);
} finally { } finally {
// make sure to always push / pop, even if the increment is corrupted // make sure to always push / pop, even if the increment is corrupted
push(historyEntry); push(historyEntry);
@ -187,10 +181,9 @@ export class HistoryEntry {
elements: SceneElementsMap, elements: SceneElementsMap,
appState: AppState, appState: AppState,
snapshot: Readonly<Snapshot>, snapshot: Readonly<Snapshot>,
scene: Scene,
): [SceneElementsMap, AppState, boolean] { ): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = const [nextElements, elementsContainVisibleChange] =
this.elementsChange.applyTo(elements, snapshot.elements, scene); this.elementsChange.applyTo(elements, snapshot.elements);
const [nextAppState, appStateContainsVisibleChange] = const [nextAppState, appStateContainsVisibleChange] =
this.appStateChange.applyTo(appState, nextElements); this.appStateChange.applyTo(appState, nextElements);

View file

@ -44,7 +44,6 @@ import { queryByText } from "@testing-library/react";
import { HistoryEntry } from "../history"; import { HistoryEntry } from "../history";
import { AppStateChange, ElementsChange } from "../change"; import { AppStateChange, ElementsChange } from "../change";
import { Snapshot, StoreAction } from "../store"; import { Snapshot, StoreAction } from "../store";
import type Scene from "../scene/Scene";
const { h } = window; const { h } = window;
@ -118,7 +117,6 @@ describe("history", () => {
arrayToMap(h.elements) as SceneElementsMap, arrayToMap(h.elements) as SceneElementsMap,
appState, appState,
Snapshot.empty(), Snapshot.empty(),
{} as Scene,
) as any, ) as any,
); );
} catch (e) { } catch (e) {
@ -140,7 +138,6 @@ describe("history", () => {
arrayToMap(h.elements) as SceneElementsMap, arrayToMap(h.elements) as SceneElementsMap,
appState, appState,
Snapshot.empty(), Snapshot.empty(),
{} as Scene,
) as any, ) as any,
); );
} catch (e) { } catch (e) {

View file

@ -5,6 +5,7 @@ import type {
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
FontString, FontString,
SceneElementsMap,
} from "../element/types"; } from "../element/types";
import { Excalidraw, mutateElement } from "../index"; import { Excalidraw, mutateElement } from "../index";
import { centerPoint } from "../math"; import { centerPoint } from "../math";
@ -1344,7 +1345,7 @@ describe("Test Linear Elements", () => {
], ],
}, },
], ],
h.scene, new Map() as SceneElementsMap,
); );
}); });
expect(line.x).toBe(origStartX + 10); expect(line.x).toBe(origStartX + 10);