mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: multiplayer undo / redo (#7348)
This commit is contained in:
parent
5211b003b8
commit
530617be90
71 changed files with 34885 additions and 14877 deletions
|
@ -9,7 +9,6 @@ import {
|
|||
ExcalidrawElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
|
@ -21,6 +20,8 @@ import {
|
|||
NonDeletedExcalidrawElement,
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawArrowElement,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
|
@ -28,11 +29,14 @@ import { AppClassProperties, AppState, Point } from "../types";
|
|||
import { isPointOnShape } from "../../utils/collision";
|
||||
import { getElementAtPosition } from "../scene";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { ElementUpdate, mutateElement } from "./mutateElement";
|
||||
import Scene from "../scene/Scene";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
|
@ -365,71 +369,71 @@ const calculateFocusAndGap = (
|
|||
export const updateBoundElements = (
|
||||
changedElement: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
},
|
||||
) => {
|
||||
const boundLinearElements = (changedElement.boundElements ?? []).filter(
|
||||
(el) => el.type === "arrow",
|
||||
);
|
||||
if (boundLinearElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
const scene = Scene.getScene(changedElement)!;
|
||||
getNonDeletedElements(
|
||||
scene,
|
||||
boundLinearElements.map((el) => el.id),
|
||||
).forEach((element) => {
|
||||
if (!isLinearElement(element)) {
|
||||
|
||||
if (!isBindableElement(changedElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||
if (!isLinearElement(element) || element.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bindableElement = changedElement as ExcalidrawBindableElement;
|
||||
// In case the boundElements are stale
|
||||
if (!doesNeedUpdate(element, bindableElement)) {
|
||||
if (!doesNeedUpdate(element, changedElement)) {
|
||||
return;
|
||||
}
|
||||
const startBinding = maybeCalculateNewGapWhenScaling(
|
||||
bindableElement,
|
||||
element.startBinding,
|
||||
newSize,
|
||||
);
|
||||
const endBinding = maybeCalculateNewGapWhenScaling(
|
||||
bindableElement,
|
||||
element.endBinding,
|
||||
newSize,
|
||||
);
|
||||
const bindings = {
|
||||
startBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.startBinding,
|
||||
newSize,
|
||||
),
|
||||
endBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.endBinding,
|
||||
newSize,
|
||||
),
|
||||
};
|
||||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, { startBinding, endBinding });
|
||||
mutateElement(element, bindings);
|
||||
return;
|
||||
}
|
||||
updateBoundPoint(
|
||||
element,
|
||||
"start",
|
||||
startBinding,
|
||||
changedElement as ExcalidrawBindableElement,
|
||||
|
||||
bindableElementsVisitor(
|
||||
elementsMap,
|
||||
);
|
||||
updateBoundPoint(
|
||||
element,
|
||||
"end",
|
||||
endBinding,
|
||||
changedElement as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
(bindableElement, bindingProp) => {
|
||||
if (
|
||||
bindableElement &&
|
||||
isBindableElement(bindableElement) &&
|
||||
(bindingProp === "startBinding" || bindingProp === "endBinding")
|
||||
) {
|
||||
updateBoundPoint(
|
||||
element,
|
||||
bindingProp,
|
||||
bindings[bindingProp],
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
const boundText = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundText) {
|
||||
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !boundText.isDeleted) {
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -452,26 +456,21 @@ const getSimultaneouslyUpdatedElementIds = (
|
|||
|
||||
const updateBoundPoint = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
binding: PointBinding | null | undefined,
|
||||
changedElement: ExcalidrawBindableElement,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
): void => {
|
||||
if (
|
||||
binding == null ||
|
||||
// We only need to update the other end if this is a 2 point line element
|
||||
(binding.elementId !== changedElement.id && linearElement.points.length > 2)
|
||||
(binding.elementId !== bindableElement.id &&
|
||||
linearElement.points.length > 2)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bindingElement = Scene.getScene(linearElement)!.getElement(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement | null;
|
||||
if (bindingElement == null) {
|
||||
// We're not cleaning up after deleted elements atm., so handle this case
|
||||
return;
|
||||
}
|
||||
const direction = startOrEnd === "start" ? -1 : 1;
|
||||
|
||||
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||
const adjacentPointIndex = edgePointIndex - direction;
|
||||
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
|
@ -480,7 +479,7 @@ const updateBoundPoint = (
|
|||
elementsMap,
|
||||
);
|
||||
const focusPointAbsolute = determineFocusPoint(
|
||||
bindingElement,
|
||||
bindableElement,
|
||||
binding.focus,
|
||||
adjacentPoint,
|
||||
elementsMap,
|
||||
|
@ -492,7 +491,7 @@ const updateBoundPoint = (
|
|||
newEdgePoint = focusPointAbsolute;
|
||||
} else {
|
||||
const intersections = intersectElementWithLine(
|
||||
bindingElement,
|
||||
bindableElement,
|
||||
adjacentPoint,
|
||||
focusPointAbsolute,
|
||||
binding.gap,
|
||||
|
@ -519,7 +518,7 @@ const updateBoundPoint = (
|
|||
),
|
||||
},
|
||||
],
|
||||
{ [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding },
|
||||
{ [startOrEnd]: binding },
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -773,70 +772,37 @@ export const fixBindingsAfterDeletion = (
|
|||
sceneElements: readonly ExcalidrawElement[],
|
||||
deletedElements: readonly ExcalidrawElement[],
|
||||
): void => {
|
||||
const deletedElementIds = new Set(
|
||||
deletedElements.map((element) => element.id),
|
||||
);
|
||||
// non-deleted which bindings need to be updated
|
||||
const affectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||
deletedElements.forEach((deletedElement) => {
|
||||
if (isBindableElement(deletedElement)) {
|
||||
deletedElement.boundElements?.forEach((element) => {
|
||||
if (!deletedElementIds.has(element.id)) {
|
||||
affectedElements.add(element.id);
|
||||
}
|
||||
});
|
||||
} else if (isBindingElement(deletedElement)) {
|
||||
if (deletedElement.startBinding) {
|
||||
affectedElements.add(deletedElement.startBinding.elementId);
|
||||
}
|
||||
if (deletedElement.endBinding) {
|
||||
affectedElements.add(deletedElement.endBinding.elementId);
|
||||
}
|
||||
}
|
||||
});
|
||||
sceneElements
|
||||
.filter(({ id }) => affectedElements.has(id))
|
||||
.forEach((element) => {
|
||||
if (isBindableElement(element)) {
|
||||
mutateElement(element, {
|
||||
boundElements: newBoundElementsAfterDeletion(
|
||||
element.boundElements,
|
||||
deletedElementIds,
|
||||
),
|
||||
});
|
||||
} else if (isBindingElement(element)) {
|
||||
mutateElement(element, {
|
||||
startBinding: newBindingAfterDeletion(
|
||||
element.startBinding,
|
||||
deletedElementIds,
|
||||
),
|
||||
endBinding: newBindingAfterDeletion(
|
||||
element.endBinding,
|
||||
deletedElementIds,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const elements = arrayToMap(sceneElements);
|
||||
|
||||
const newBindingAfterDeletion = (
|
||||
binding: PointBinding | null,
|
||||
deletedElementIds: Set<ExcalidrawElement["id"]>,
|
||||
): PointBinding | null => {
|
||||
if (binding == null || deletedElementIds.has(binding.elementId)) {
|
||||
return null;
|
||||
for (const element of deletedElements) {
|
||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
||||
}
|
||||
return binding;
|
||||
};
|
||||
|
||||
const newBoundElementsAfterDeletion = (
|
||||
const newBoundElements = (
|
||||
boundElements: ExcalidrawElement["boundElements"],
|
||||
deletedElementIds: Set<ExcalidrawElement["id"]>,
|
||||
idsToRemove: Set<ExcalidrawElement["id"]>,
|
||||
elementsToAdd: Array<ExcalidrawElement> = [],
|
||||
) => {
|
||||
if (!boundElements) {
|
||||
return null;
|
||||
}
|
||||
return boundElements.filter((ele) => !deletedElementIds.has(ele.id));
|
||||
|
||||
const nextBoundElements = boundElements.filter(
|
||||
(boundElement) => !idsToRemove.has(boundElement.id),
|
||||
);
|
||||
|
||||
nextBoundElements.push(
|
||||
...elementsToAdd.map(
|
||||
(x) =>
|
||||
({ id: x.id, type: x.type } as
|
||||
| ExcalidrawArrowElement
|
||||
| ExcalidrawTextElement),
|
||||
),
|
||||
);
|
||||
|
||||
return nextBoundElements;
|
||||
};
|
||||
|
||||
export const bindingBorderTest = (
|
||||
|
@ -1382,3 +1348,306 @@ export const findFocusPointForRectangulars = (
|
|||
});
|
||||
return tangentPoint!;
|
||||
};
|
||||
export const bindingProperties: Set<BindableProp | BindingProp> = new Set([
|
||||
"boundElements",
|
||||
"frameId",
|
||||
"containerId",
|
||||
"startBinding",
|
||||
"endBinding",
|
||||
]);
|
||||
|
||||
export type BindableProp = "boundElements";
|
||||
|
||||
export type BindingProp =
|
||||
| "frameId"
|
||||
| "containerId"
|
||||
| "startBinding"
|
||||
| "endBinding";
|
||||
|
||||
type BoundElementsVisitingFunc = (
|
||||
boundElement: ExcalidrawElement | undefined,
|
||||
bindingProp: BindableProp,
|
||||
bindingId: string,
|
||||
) => void;
|
||||
|
||||
type BindableElementVisitingFunc = (
|
||||
bindableElement: ExcalidrawElement | undefined,
|
||||
bindingProp: BindingProp,
|
||||
bindingId: string,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Tries to visit each bound element (does not have to be found).
|
||||
*/
|
||||
const boundElementsVisitor = (
|
||||
elements: ElementsMap,
|
||||
element: ExcalidrawElement,
|
||||
visit: BoundElementsVisitingFunc,
|
||||
) => {
|
||||
if (isBindableElement(element)) {
|
||||
// create new instance so that possible mutations won't play a role in visiting order
|
||||
const boundElements = element.boundElements?.slice() ?? [];
|
||||
|
||||
// last added text should be the one we keep (~previous are duplicates)
|
||||
boundElements.forEach(({ id }) => {
|
||||
visit(elements.get(id), "boundElements", id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to visit each bindable element (does not have to be found).
|
||||
*/
|
||||
const bindableElementsVisitor = (
|
||||
elements: ElementsMap,
|
||||
element: ExcalidrawElement,
|
||||
visit: BindableElementVisitingFunc,
|
||||
) => {
|
||||
if (element.frameId) {
|
||||
const id = element.frameId;
|
||||
visit(elements.get(id), "frameId", id);
|
||||
}
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
const id = element.containerId;
|
||||
visit(elements.get(id), "containerId", id);
|
||||
}
|
||||
|
||||
if (isArrowElement(element)) {
|
||||
if (element.startBinding) {
|
||||
const id = element.startBinding.elementId;
|
||||
visit(elements.get(id), "startBinding", id);
|
||||
}
|
||||
|
||||
if (element.endBinding) {
|
||||
const id = element.endBinding.elementId;
|
||||
visit(elements.get(id), "endBinding", id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bound element containing bindings to `frameId`, `containerId`, `startBinding` or `endBinding`.
|
||||
*/
|
||||
export class BoundElement {
|
||||
/**
|
||||
* Unbind the affected non deleted bindable elements (removing element from `boundElements`).
|
||||
* - iterates non deleted bindable elements (`containerId` | `startBinding.elementId` | `endBinding.elementId`) of the current element
|
||||
* - prepares updates to unbind each bindable element's `boundElements` from the current element
|
||||
*/
|
||||
public static unbindAffected(
|
||||
elements: ElementsMap,
|
||||
boundElement: ExcalidrawElement | undefined,
|
||||
updateElementWith: (
|
||||
affected: ExcalidrawElement,
|
||||
updates: ElementUpdate<ExcalidrawElement>,
|
||||
) => void,
|
||||
) {
|
||||
if (!boundElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindableElementsVisitor(elements, boundElement, (bindableElement) => {
|
||||
// bindable element is deleted, this is fine
|
||||
if (!bindableElement || bindableElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
boundElementsVisitor(
|
||||
elements,
|
||||
bindableElement,
|
||||
(_, __, boundElementId) => {
|
||||
if (boundElementId === boundElement.id) {
|
||||
updateElementWith(bindableElement, {
|
||||
boundElements: newBoundElements(
|
||||
bindableElement.boundElements,
|
||||
new Set([boundElementId]),
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebind the next affected non deleted bindable elements (adding element to `boundElements`).
|
||||
* - iterates non deleted bindable elements (`containerId` | `startBinding.elementId` | `endBinding.elementId`) of the current element
|
||||
* - prepares updates to rebind each bindable element's `boundElements` to the current element
|
||||
*
|
||||
* NOTE: rebind expects that affected elements were previously unbound with `BoundElement.unbindAffected`
|
||||
*/
|
||||
public static rebindAffected = (
|
||||
elements: ElementsMap,
|
||||
boundElement: ExcalidrawElement | undefined,
|
||||
updateElementWith: (
|
||||
affected: ExcalidrawElement,
|
||||
updates: ElementUpdate<ExcalidrawElement>,
|
||||
) => void,
|
||||
) => {
|
||||
// don't try to rebind element that is deleted
|
||||
if (!boundElement || boundElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindableElementsVisitor(
|
||||
elements,
|
||||
boundElement,
|
||||
(bindableElement, bindingProp) => {
|
||||
// unbind from bindable elements, as bindings from non deleted elements into deleted elements are incorrect
|
||||
if (!bindableElement || bindableElement.isDeleted) {
|
||||
updateElementWith(boundElement, { [bindingProp]: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// frame bindings are unidirectional, there is nothing to rebind
|
||||
if (bindingProp === "frameId") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
bindableElement.boundElements?.find((x) => x.id === boundElement.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArrowElement(boundElement)) {
|
||||
// rebind if not found!
|
||||
updateElementWith(bindableElement, {
|
||||
boundElements: newBoundElements(
|
||||
bindableElement.boundElements,
|
||||
new Set(),
|
||||
new Array(boundElement),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (isTextElement(boundElement)) {
|
||||
if (!bindableElement.boundElements?.find((x) => x.type === "text")) {
|
||||
// rebind only if there is no other text bound already
|
||||
updateElementWith(bindableElement, {
|
||||
boundElements: newBoundElements(
|
||||
bindableElement.boundElements,
|
||||
new Set(),
|
||||
new Array(boundElement),
|
||||
),
|
||||
});
|
||||
} else {
|
||||
// unbind otherwise
|
||||
updateElementWith(boundElement, { [bindingProp]: null });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindable element containing bindings to `boundElements`.
|
||||
*/
|
||||
export class BindableElement {
|
||||
/**
|
||||
* Unbind the affected non deleted bound elements (resetting `containerId`, `startBinding`, `endBinding` to `null`).
|
||||
* - iterates through non deleted `boundElements` of the current element
|
||||
* - prepares updates to unbind each bound element from the current element
|
||||
*/
|
||||
public static unbindAffected(
|
||||
elements: ElementsMap,
|
||||
bindableElement: ExcalidrawElement | undefined,
|
||||
updateElementWith: (
|
||||
affected: ExcalidrawElement,
|
||||
updates: ElementUpdate<ExcalidrawElement>,
|
||||
) => void,
|
||||
) {
|
||||
if (!bindableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
boundElementsVisitor(elements, bindableElement, (boundElement) => {
|
||||
// bound element is deleted, this is fine
|
||||
if (!boundElement || boundElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindableElementsVisitor(
|
||||
elements,
|
||||
boundElement,
|
||||
(_, bindingProp, bindableElementId) => {
|
||||
// making sure there is an element to be unbound
|
||||
if (bindableElementId === bindableElement.id) {
|
||||
updateElementWith(boundElement, { [bindingProp]: null });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebind the affected non deleted bound elements (for now setting only `containerId`, as we cannot rebind arrows atm).
|
||||
* - iterates through non deleted `boundElements` of the current element
|
||||
* - prepares updates to rebind each bound element to the current element or unbind it from `boundElements` in case of conflicts
|
||||
*
|
||||
* NOTE: rebind expects that affected elements were previously unbound with `BindaleElement.unbindAffected`
|
||||
*/
|
||||
public static rebindAffected = (
|
||||
elements: ElementsMap,
|
||||
bindableElement: ExcalidrawElement | undefined,
|
||||
updateElementWith: (
|
||||
affected: ExcalidrawElement,
|
||||
updates: ElementUpdate<ExcalidrawElement>,
|
||||
) => void,
|
||||
) => {
|
||||
// don't try to rebind element that is deleted (i.e. updated as deleted)
|
||||
if (!bindableElement || bindableElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
boundElementsVisitor(
|
||||
elements,
|
||||
bindableElement,
|
||||
(boundElement, _, boundElementId) => {
|
||||
// unbind from bindable elements, as bindings from non deleted elements into deleted elements are incorrect
|
||||
if (!boundElement || boundElement.isDeleted) {
|
||||
updateElementWith(bindableElement, {
|
||||
boundElements: newBoundElements(
|
||||
bindableElement.boundElements,
|
||||
new Set([boundElementId]),
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextElement(boundElement)) {
|
||||
const boundElements = bindableElement.boundElements?.slice() ?? [];
|
||||
// check if this is the last element in the array, if not, there is an previously bound text which should be unbound
|
||||
if (
|
||||
boundElements.reverse().find((x) => x.type === "text")?.id ===
|
||||
boundElement.id
|
||||
) {
|
||||
if (boundElement.containerId !== bindableElement.id) {
|
||||
// rebind if not bound already!
|
||||
updateElementWith(boundElement, {
|
||||
containerId: bindableElement.id,
|
||||
} as ElementUpdate<ExcalidrawTextElement>);
|
||||
}
|
||||
} else {
|
||||
if (boundElement.containerId !== null) {
|
||||
// unbind if not unbound already
|
||||
updateElementWith(boundElement, {
|
||||
containerId: null,
|
||||
} as ElementUpdate<ExcalidrawTextElement>);
|
||||
}
|
||||
|
||||
// unbind from boundElements as the element got bound to some other element in the meantime
|
||||
updateElementWith(bindableElement, {
|
||||
boundElements: newBoundElements(
|
||||
bindableElement.boundElements,
|
||||
new Set([boundElement.id]),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "./types";
|
||||
import { sanitizeHTMLAttribute } from "../data/url";
|
||||
import { MarkRequired } from "../utility-types";
|
||||
import { StoreAction } from "../store";
|
||||
|
||||
type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
|
||||
|
||||
|
@ -314,7 +315,7 @@ export const actionSetEmbeddableAsActiveTool = register({
|
|||
type: "embeddable",
|
||||
}),
|
||||
},
|
||||
commitToHistory: false,
|
||||
storeAction: StoreAction.NONE,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,7 +36,6 @@ import {
|
|||
AppClassProperties,
|
||||
} from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import History from "../history";
|
||||
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
|
@ -50,6 +49,7 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
|||
import { DRAGGING_THRESHOLD } from "../constants";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { IStore } from "../store";
|
||||
|
||||
const editorMidPointsCache: {
|
||||
version: number | null;
|
||||
|
@ -642,7 +642,7 @@ export class LinearElementEditor {
|
|||
static handlePointerDown(
|
||||
event: React.PointerEvent<HTMLElement>,
|
||||
appState: AppState,
|
||||
history: History,
|
||||
store: IStore,
|
||||
scenePointer: { x: number; y: number },
|
||||
linearElementEditor: LinearElementEditor,
|
||||
app: AppClassProperties,
|
||||
|
@ -700,7 +700,7 @@ export class LinearElementEditor {
|
|||
});
|
||||
ret.didAddPoint = true;
|
||||
}
|
||||
history.resumeRecording();
|
||||
store.shouldCaptureIncrement();
|
||||
ret.linearElementEditor = {
|
||||
...linearElementEditor,
|
||||
pointerDownState: {
|
||||
|
|
|
@ -7,9 +7,9 @@ import { getUpdatedTimestamp } from "../utils";
|
|||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"id" | "version" | "versionNonce"
|
||||
"id" | "version" | "versionNonce" | "updated"
|
||||
>;
|
||||
|
||||
// This function tracks updates of text elements for the purposes for collaboration.
|
||||
|
@ -79,6 +79,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return element;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ import { AppState, Zoom } from "../types";
|
|||
import { getElementBounds } from "./bounds";
|
||||
import { viewportCoordsToSceneCoords } from "../utils";
|
||||
|
||||
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
||||
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
||||
// - could also be part of `_clearElements`
|
||||
export const isInvisiblySmallElement = (
|
||||
element: ExcalidrawElement,
|
||||
): boolean => {
|
||||
|
|
|
@ -48,6 +48,7 @@ export const redrawTextBoundingBox = (
|
|||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
informMutation: boolean = true,
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
const boundTextUpdates = {
|
||||
|
@ -56,6 +57,7 @@ export const redrawTextBoundingBox = (
|
|||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
angle: container?.angle ?? textElement.angle,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
|
@ -89,7 +91,7 @@ export const redrawTextBoundingBox = (
|
|||
metrics.height,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { height: nextHeight });
|
||||
mutateElement(container, { height: nextHeight }, informMutation);
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
}
|
||||
if (metrics.width > maxContainerWidth) {
|
||||
|
@ -97,7 +99,7 @@ export const redrawTextBoundingBox = (
|
|||
metrics.width,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { width: nextWidth });
|
||||
mutateElement(container, { width: nextWidth }, informMutation);
|
||||
}
|
||||
const updatedTextElement = {
|
||||
...textElement,
|
||||
|
@ -112,7 +114,7 @@ export const redrawTextBoundingBox = (
|
|||
boundTextUpdates.y = y;
|
||||
}
|
||||
|
||||
mutateElement(textElement, boundTextUpdates);
|
||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
||||
};
|
||||
|
||||
export const bindTextToShapeAfterDuplication = (
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
ExcalidrawIframeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
ExcalidrawMagicFrameElement,
|
||||
ExcalidrawArrowElement,
|
||||
} from "./types";
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
|
@ -101,7 +102,7 @@ export const isLinearElement = (
|
|||
|
||||
export const isArrowElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawLinearElement => {
|
||||
): element is ExcalidrawArrowElement => {
|
||||
return element != null && element.type === "arrow";
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@ type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
|||
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
||||
export type FractionalIndex = string & { _brand: "franctionalIndex" };
|
||||
|
||||
export type BoundElement = Readonly<{
|
||||
id: ExcalidrawLinearElement["id"];
|
||||
type: "arrow" | "text";
|
||||
}>;
|
||||
|
||||
type _ExcalidrawElementBase = Readonly<{
|
||||
id: string;
|
||||
x: number;
|
||||
|
@ -62,12 +67,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||
groupIds: readonly GroupId[];
|
||||
frameId: string | null;
|
||||
/** other elements that are bound to this element */
|
||||
boundElements:
|
||||
| readonly Readonly<{
|
||||
id: ExcalidrawLinearElement["id"];
|
||||
type: "arrow" | "text";
|
||||
}>[]
|
||||
| null;
|
||||
boundElements: readonly BoundElement[] | null;
|
||||
/** epoch (ms) timestamp of last element update */
|
||||
updated: number;
|
||||
link: string | null;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue