mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
feat: various delta improvements (#9571)
This commit is contained in:
@ -205,6 +205,7 @@ describe("collaboration", () => {
|
|||||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(API.getSnapshot()).toEqual([
|
expect(API.getSnapshot()).toEqual([
|
||||||
expect.objectContaining(rect1Props),
|
expect.objectContaining(rect1Props),
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||||
@ -247,79 +248,5 @@ describe("collaboration", () => {
|
|||||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// simulate local update
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([
|
|
||||||
h.elements[0],
|
|
||||||
newElementWith(h.elements[1], { x: 100 }),
|
|
||||||
]),
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// we expect to iterate the stack to the first visible change
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// simulate force deleting the element remotely
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([rect1]),
|
|
||||||
captureUpdate: CaptureUpdateAction.NEVER,
|
|
||||||
});
|
|
||||||
|
|
||||||
// snapshot was correctly updated and marked the element as deleted
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
|
||||||
|
|
||||||
// with explicit redo (as update) we again restored the element from the snapshot!
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -714,8 +714,8 @@ export const arrayToObject = <T>(
|
|||||||
array: readonly T[],
|
array: readonly T[],
|
||||||
groupBy?: (value: T) => string | number,
|
groupBy?: (value: T) => string | number,
|
||||||
) =>
|
) =>
|
||||||
array.reduce((acc, value) => {
|
array.reduce((acc, value, idx) => {
|
||||||
acc[groupBy ? groupBy(value) : String(value)] = value;
|
acc[groupBy ? groupBy(value) : idx] = value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: T });
|
}, {} as { [key: string]: T });
|
||||||
|
|
||||||
|
@ -5,11 +5,12 @@ import {
|
|||||||
isDevEnv,
|
isDevEnv,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
randomInteger,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
@ -18,7 +19,12 @@ import type {
|
|||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { DTO, SubtypeOf, ValueOf } from "@excalidraw/common/utility-types";
|
import type {
|
||||||
|
DTO,
|
||||||
|
Mutable,
|
||||||
|
SubtypeOf,
|
||||||
|
ValueOf,
|
||||||
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
@ -51,6 +57,8 @@ import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
|||||||
|
|
||||||
import { Scene } from "./Scene";
|
import { Scene } from "./Scene";
|
||||||
|
|
||||||
|
import { StoreSnapshot } from "./store";
|
||||||
|
|
||||||
import type { BindableProp, BindingProp } from "./binding";
|
import type { BindableProp, BindingProp } from "./binding";
|
||||||
|
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
@ -73,13 +81,20 @@ export class Delta<T> {
|
|||||||
public static create<T>(
|
public static create<T>(
|
||||||
deleted: Partial<T>,
|
deleted: Partial<T>,
|
||||||
inserted: Partial<T>,
|
inserted: Partial<T>,
|
||||||
modifier?: (delta: Partial<T>) => Partial<T>,
|
modifier?: (
|
||||||
modifierOptions?: "deleted" | "inserted",
|
delta: Partial<T>,
|
||||||
|
partialType: "deleted" | "inserted",
|
||||||
|
) => Partial<T>,
|
||||||
|
modifierOptions?: "deleted" | "inserted" | "both",
|
||||||
) {
|
) {
|
||||||
const modifiedDeleted =
|
const modifiedDeleted =
|
||||||
modifier && modifierOptions !== "inserted" ? modifier(deleted) : deleted;
|
modifier && modifierOptions !== "inserted"
|
||||||
|
? modifier(deleted, "deleted")
|
||||||
|
: deleted;
|
||||||
const modifiedInserted =
|
const modifiedInserted =
|
||||||
modifier && modifierOptions !== "deleted" ? modifier(inserted) : inserted;
|
modifier && modifierOptions !== "deleted"
|
||||||
|
? modifier(inserted, "inserted")
|
||||||
|
: inserted;
|
||||||
|
|
||||||
return new Delta(modifiedDeleted, modifiedInserted);
|
return new Delta(modifiedDeleted, modifiedInserted);
|
||||||
}
|
}
|
||||||
@ -113,11 +128,7 @@ export class Delta<T> {
|
|||||||
// - we do this only on previously detected changed elements
|
// - we do this only on previously detected changed elements
|
||||||
// - we do shallow compare only on the first level of properties (not going any deeper)
|
// - we do shallow compare only on the first level of properties (not going any deeper)
|
||||||
// - # of properties is reasonably small
|
// - # of properties is reasonably small
|
||||||
for (const key of this.distinctKeysIterator(
|
for (const key of this.getDifferences(prevObject, nextObject)) {
|
||||||
"full",
|
|
||||||
prevObject,
|
|
||||||
nextObject,
|
|
||||||
)) {
|
|
||||||
deleted[key as keyof T] = prevObject[key];
|
deleted[key as keyof T] = prevObject[key];
|
||||||
inserted[key as keyof T] = nextObject[key];
|
inserted[key as keyof T] = nextObject[key];
|
||||||
}
|
}
|
||||||
@ -256,12 +267,14 @@ export class Delta<T> {
|
|||||||
arrayToObject(deletedArray, groupBy),
|
arrayToObject(deletedArray, groupBy),
|
||||||
arrayToObject(insertedArray, groupBy),
|
arrayToObject(insertedArray, groupBy),
|
||||||
),
|
),
|
||||||
|
(x) => x,
|
||||||
);
|
);
|
||||||
const insertedDifferences = arrayToObject(
|
const insertedDifferences = arrayToObject(
|
||||||
Delta.getRightDifferences(
|
Delta.getRightDifferences(
|
||||||
arrayToObject(deletedArray, groupBy),
|
arrayToObject(deletedArray, groupBy),
|
||||||
arrayToObject(insertedArray, groupBy),
|
arrayToObject(insertedArray, groupBy),
|
||||||
),
|
),
|
||||||
|
(x) => x,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -320,6 +333,42 @@ export class Delta<T> {
|
|||||||
return !!anyDistinctKey;
|
return !!anyDistinctKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares if shared properties of object1 and object2 contain any different value (aka inner join).
|
||||||
|
*/
|
||||||
|
public static isInnerDifferent<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
): boolean {
|
||||||
|
const anyDistinctKey = !!this.distinctKeysIterator(
|
||||||
|
"inner",
|
||||||
|
object1,
|
||||||
|
object2,
|
||||||
|
skipShallowCompare,
|
||||||
|
).next().value;
|
||||||
|
|
||||||
|
return !!anyDistinctKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares if any properties of object1 and object2 contain any different value (aka full join).
|
||||||
|
*/
|
||||||
|
public static isDifferent<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
): boolean {
|
||||||
|
const anyDistinctKey = !!this.distinctKeysIterator(
|
||||||
|
"full",
|
||||||
|
object1,
|
||||||
|
object2,
|
||||||
|
skipShallowCompare,
|
||||||
|
).next().value;
|
||||||
|
|
||||||
|
return !!anyDistinctKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns sorted object1 keys that have distinct values.
|
* Returns sorted object1 keys that have distinct values.
|
||||||
*/
|
*/
|
||||||
@ -346,6 +395,32 @@ export class Delta<T> {
|
|||||||
).sort();
|
).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns sorted keys of shared object1 and object2 properties that have distinct values (aka inner join).
|
||||||
|
*/
|
||||||
|
public static getInnerDifferences<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
) {
|
||||||
|
return Array.from(
|
||||||
|
this.distinctKeysIterator("inner", object1, object2, skipShallowCompare),
|
||||||
|
).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns sorted keys that have distinct values between object1 and object2 (aka full join).
|
||||||
|
*/
|
||||||
|
public static getDifferences<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
) {
|
||||||
|
return Array.from(
|
||||||
|
this.distinctKeysIterator("full", object1, object2, skipShallowCompare),
|
||||||
|
).sort();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterator comparing values of object properties based on the passed joining strategy.
|
* Iterator comparing values of object properties based on the passed joining strategy.
|
||||||
*
|
*
|
||||||
@ -354,7 +429,7 @@ export class Delta<T> {
|
|||||||
* WARN: it's based on shallow compare performed only on the first level and doesn't go deeper than that.
|
* WARN: it's based on shallow compare performed only on the first level and doesn't go deeper than that.
|
||||||
*/
|
*/
|
||||||
private static *distinctKeysIterator<T extends {}>(
|
private static *distinctKeysIterator<T extends {}>(
|
||||||
join: "left" | "right" | "full",
|
join: "left" | "right" | "inner" | "full",
|
||||||
object1: T,
|
object1: T,
|
||||||
object2: T,
|
object2: T,
|
||||||
skipShallowCompare = false,
|
skipShallowCompare = false,
|
||||||
@ -369,6 +444,8 @@ export class Delta<T> {
|
|||||||
keys = Object.keys(object1);
|
keys = Object.keys(object1);
|
||||||
} else if (join === "right") {
|
} else if (join === "right") {
|
||||||
keys = Object.keys(object2);
|
keys = Object.keys(object2);
|
||||||
|
} else if (join === "inner") {
|
||||||
|
keys = Object.keys(object1).filter((key) => key in object2);
|
||||||
} else if (join === "full") {
|
} else if (join === "full") {
|
||||||
keys = Array.from(
|
keys = Array.from(
|
||||||
new Set([...Object.keys(object1), ...Object.keys(object2)]),
|
new Set([...Object.keys(object1), ...Object.keys(object2)]),
|
||||||
@ -382,17 +459,17 @@ export class Delta<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const object1Value = object1[key as keyof T];
|
const value1 = object1[key as keyof T];
|
||||||
const object2Value = object2[key as keyof T];
|
const value2 = object2[key as keyof T];
|
||||||
|
|
||||||
if (object1Value !== object2Value) {
|
if (value1 !== value2) {
|
||||||
if (
|
if (
|
||||||
!skipShallowCompare &&
|
!skipShallowCompare &&
|
||||||
typeof object1Value === "object" &&
|
typeof value1 === "object" &&
|
||||||
typeof object2Value === "object" &&
|
typeof value2 === "object" &&
|
||||||
object1Value !== null &&
|
value1 !== null &&
|
||||||
object2Value !== null &&
|
value2 !== null &&
|
||||||
isShallowEqual(object1Value, object2Value)
|
isShallowEqual(value1, value2)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -858,10 +935,17 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
|
type ElementPartial<TElement extends ExcalidrawElement = ExcalidrawElement> =
|
||||||
ElementUpdate<Ordered<T>>,
|
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
||||||
"seed"
|
|
||||||
>;
|
export type ApplyToOptions = {
|
||||||
|
excludedProperties: Set<keyof ElementPartial>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApplyToFlags = {
|
||||||
|
containsVisibleDifference: boolean;
|
||||||
|
containsZindexDifference: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elements change is a low level primitive to capture a change between two sets of elements.
|
* Elements change is a low level primitive to capture a change between two sets of elements.
|
||||||
@ -944,13 +1028,33 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
inserted,
|
inserted,
|
||||||
}: Delta<ElementPartial>) => !!deleted.isDeleted === !!inserted.isDeleted;
|
}: Delta<ElementPartial>) => !!deleted.isDeleted === !!inserted.isDeleted;
|
||||||
|
|
||||||
|
private static satisfiesCommmonInvariants = ({
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
}: Delta<ElementPartial>) =>
|
||||||
|
!!(
|
||||||
|
deleted.version &&
|
||||||
|
inserted.version &&
|
||||||
|
// versions are required integers
|
||||||
|
Number.isInteger(deleted.version) &&
|
||||||
|
Number.isInteger(inserted.version) &&
|
||||||
|
// versions should be positive, zero included
|
||||||
|
deleted.version >= 0 &&
|
||||||
|
inserted.version >= 0 &&
|
||||||
|
// versions should never be the same
|
||||||
|
deleted.version !== inserted.version
|
||||||
|
);
|
||||||
|
|
||||||
private static validate(
|
private static validate(
|
||||||
elementsDelta: ElementsDelta,
|
elementsDelta: ElementsDelta,
|
||||||
type: "added" | "removed" | "updated",
|
type: "added" | "removed" | "updated",
|
||||||
satifies: (delta: Delta<ElementPartial>) => boolean,
|
satifiesSpecialInvariants: (delta: Delta<ElementPartial>) => boolean,
|
||||||
) {
|
) {
|
||||||
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
||||||
if (!satifies(delta)) {
|
if (
|
||||||
|
!this.satisfiesCommmonInvariants(delta) ||
|
||||||
|
!satifiesSpecialInvariants(delta)
|
||||||
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`Broken invariant for "${type}" delta, element "${id}", delta:`,
|
`Broken invariant for "${type}" delta, element "${id}", delta:`,
|
||||||
delta,
|
delta,
|
||||||
@ -986,7 +1090,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
||||||
const inserted = { isDeleted: true } as ElementPartial;
|
|
||||||
|
const inserted = {
|
||||||
|
isDeleted: true,
|
||||||
|
version: prevElement.version + 1,
|
||||||
|
versionNonce: randomInteger(),
|
||||||
|
} as ElementPartial;
|
||||||
|
|
||||||
const delta = Delta.create(
|
const delta = Delta.create(
|
||||||
deleted,
|
deleted,
|
||||||
@ -1002,7 +1111,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const prevElement = prevElements.get(nextElement.id);
|
const prevElement = prevElements.get(nextElement.id);
|
||||||
|
|
||||||
if (!prevElement) {
|
if (!prevElement) {
|
||||||
const deleted = { isDeleted: true } as ElementPartial;
|
const deleted = {
|
||||||
|
isDeleted: true,
|
||||||
|
version: nextElement.version - 1,
|
||||||
|
versionNonce: randomInteger(),
|
||||||
|
} as ElementPartial;
|
||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
...nextElement,
|
...nextElement,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -1087,16 +1201,40 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
/**
|
/**
|
||||||
* Update delta/s based on the existing elements.
|
* Update delta/s based on the existing elements.
|
||||||
*
|
*
|
||||||
* @param elements current elements
|
* @param nextElements current elements
|
||||||
* @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated
|
* @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated
|
||||||
* @returns new instance with modified delta/s
|
* @returns new instance with modified delta/s
|
||||||
*/
|
*/
|
||||||
public applyLatestChanges(
|
public applyLatestChanges(
|
||||||
elements: SceneElementsMap,
|
prevElements: SceneElementsMap,
|
||||||
modifierOptions: "deleted" | "inserted",
|
nextElements: SceneElementsMap,
|
||||||
|
modifierOptions?: "deleted" | "inserted",
|
||||||
): ElementsDelta {
|
): ElementsDelta {
|
||||||
const modifier =
|
const modifier =
|
||||||
(element: OrderedExcalidrawElement) => (partial: ElementPartial) => {
|
(
|
||||||
|
prevElement: OrderedExcalidrawElement | undefined,
|
||||||
|
nextElement: OrderedExcalidrawElement | undefined,
|
||||||
|
) =>
|
||||||
|
(partial: ElementPartial, partialType: "deleted" | "inserted") => {
|
||||||
|
let element: OrderedExcalidrawElement | undefined;
|
||||||
|
|
||||||
|
switch (partialType) {
|
||||||
|
case "deleted":
|
||||||
|
element = prevElement;
|
||||||
|
break;
|
||||||
|
case "inserted":
|
||||||
|
element = nextElement;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the element wasn't found -> don't update the partial
|
||||||
|
if (!element) {
|
||||||
|
console.error(
|
||||||
|
`Element not found when trying to apply latest changes`,
|
||||||
|
);
|
||||||
|
return partial;
|
||||||
|
}
|
||||||
|
|
||||||
const latestPartial: { [key: string]: unknown } = {};
|
const latestPartial: { [key: string]: unknown } = {};
|
||||||
|
|
||||||
for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
|
for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
|
||||||
@ -1120,19 +1258,25 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const modifiedDeltas: Record<string, Delta<ElementPartial>> = {};
|
const modifiedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
for (const [id, delta] of Object.entries(deltas)) {
|
for (const [id, delta] of Object.entries(deltas)) {
|
||||||
const existingElement = elements.get(id);
|
const prevElement = prevElements.get(id);
|
||||||
|
const nextElement = nextElements.get(id);
|
||||||
|
|
||||||
if (existingElement) {
|
let latestDelta: Delta<ElementPartial> | null = null;
|
||||||
const modifiedDelta = Delta.create(
|
|
||||||
|
if (prevElement || nextElement) {
|
||||||
|
latestDelta = Delta.create(
|
||||||
delta.deleted,
|
delta.deleted,
|
||||||
delta.inserted,
|
delta.inserted,
|
||||||
modifier(existingElement),
|
modifier(prevElement, nextElement),
|
||||||
modifierOptions,
|
modifierOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
modifiedDeltas[id] = modifiedDelta;
|
|
||||||
} else {
|
} else {
|
||||||
modifiedDeltas[id] = delta;
|
latestDelta = delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it might happen that after applying latest changes the delta itself does not contain any changes
|
||||||
|
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
|
||||||
|
modifiedDeltas[id] = latestDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1150,12 +1294,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
public applyTo(
|
public applyTo(
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
elementsSnapshot: Map<string, OrderedExcalidrawElement>,
|
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
||||||
|
options: ApplyToOptions = {
|
||||||
|
excludedProperties: new Set(),
|
||||||
|
},
|
||||||
): [SceneElementsMap, boolean] {
|
): [SceneElementsMap, boolean] {
|
||||||
let nextElements = new Map(elements) as SceneElementsMap;
|
let nextElements = new Map(elements) as SceneElementsMap;
|
||||||
let changedElements: Map<string, OrderedExcalidrawElement>;
|
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||||
|
|
||||||
const flags = {
|
const flags: ApplyToFlags = {
|
||||||
containsVisibleDifference: false,
|
containsVisibleDifference: false,
|
||||||
containsZindexDifference: false,
|
containsZindexDifference: false,
|
||||||
};
|
};
|
||||||
@ -1164,13 +1311,14 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
try {
|
try {
|
||||||
const applyDeltas = ElementsDelta.createApplier(
|
const applyDeltas = ElementsDelta.createApplier(
|
||||||
nextElements,
|
nextElements,
|
||||||
elementsSnapshot,
|
snapshot,
|
||||||
|
options,
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addedElements = applyDeltas("added", this.added);
|
const addedElements = applyDeltas(this.added);
|
||||||
const removedElements = applyDeltas("removed", this.removed);
|
const removedElements = applyDeltas(this.removed);
|
||||||
const updatedElements = applyDeltas("updated", this.updated);
|
const updatedElements = applyDeltas(this.updated);
|
||||||
|
|
||||||
const affectedElements = this.resolveConflicts(elements, nextElements);
|
const affectedElements = this.resolveConflicts(elements, nextElements);
|
||||||
|
|
||||||
@ -1229,18 +1377,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private static createApplier =
|
private static createApplier =
|
||||||
(
|
(
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
snapshot: Map<string, OrderedExcalidrawElement>,
|
snapshot: StoreSnapshot["elements"],
|
||||||
flags: {
|
options: ApplyToOptions,
|
||||||
containsVisibleDifference: boolean;
|
flags: ApplyToFlags,
|
||||||
containsZindexDifference: boolean;
|
|
||||||
},
|
|
||||||
) =>
|
) =>
|
||||||
(
|
(deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
type: "added" | "removed" | "updated",
|
|
||||||
deltas: Record<string, Delta<ElementPartial>>,
|
|
||||||
) => {
|
|
||||||
const getElement = ElementsDelta.createGetter(
|
const getElement = ElementsDelta.createGetter(
|
||||||
type,
|
|
||||||
nextElements,
|
nextElements,
|
||||||
snapshot,
|
snapshot,
|
||||||
flags,
|
flags,
|
||||||
@ -1250,7 +1392,13 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const element = getElement(id, delta.inserted);
|
const element = getElement(id, delta.inserted);
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
const newElement = ElementsDelta.applyDelta(element, delta, flags);
|
const newElement = ElementsDelta.applyDelta(
|
||||||
|
element,
|
||||||
|
delta,
|
||||||
|
options,
|
||||||
|
flags,
|
||||||
|
);
|
||||||
|
|
||||||
nextElements.set(newElement.id, newElement);
|
nextElements.set(newElement.id, newElement);
|
||||||
acc.set(newElement.id, newElement);
|
acc.set(newElement.id, newElement);
|
||||||
}
|
}
|
||||||
@ -1261,13 +1409,9 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
private static createGetter =
|
private static createGetter =
|
||||||
(
|
(
|
||||||
type: "added" | "removed" | "updated",
|
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
snapshot: Map<string, OrderedExcalidrawElement>,
|
snapshot: StoreSnapshot["elements"],
|
||||||
flags: {
|
flags: ApplyToFlags,
|
||||||
containsVisibleDifference: boolean;
|
|
||||||
containsZindexDifference: boolean;
|
|
||||||
},
|
|
||||||
) =>
|
) =>
|
||||||
(id: string, partial: ElementPartial) => {
|
(id: string, partial: ElementPartial) => {
|
||||||
let element = elements.get(id);
|
let element = elements.get(id);
|
||||||
@ -1281,10 +1425,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
flags.containsZindexDifference = true;
|
flags.containsZindexDifference = true;
|
||||||
|
|
||||||
// as the element was force deleted, we need to check if adding it back results in a visible change
|
// as the element was force deleted, we need to check if adding it back results in a visible change
|
||||||
if (
|
if (!partial.isDeleted || (partial.isDeleted && !element.isDeleted)) {
|
||||||
partial.isDeleted === false ||
|
|
||||||
(partial.isDeleted !== true && element.isDeleted === false)
|
|
||||||
) {
|
|
||||||
flags.containsVisibleDifference = true;
|
flags.containsVisibleDifference = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1304,16 +1445,28 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private static applyDelta(
|
private static applyDelta(
|
||||||
element: OrderedExcalidrawElement,
|
element: OrderedExcalidrawElement,
|
||||||
delta: Delta<ElementPartial>,
|
delta: Delta<ElementPartial>,
|
||||||
flags: {
|
options: ApplyToOptions,
|
||||||
containsVisibleDifference: boolean;
|
flags: ApplyToFlags,
|
||||||
containsZindexDifference: boolean;
|
|
||||||
} = {
|
|
||||||
// by default we don't care about about the flags
|
|
||||||
containsVisibleDifference: true,
|
|
||||||
containsZindexDifference: true,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const { boundElements, ...directlyApplicablePartial } = delta.inserted;
|
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
||||||
|
|
||||||
|
// some properties are not directly applicable, such as:
|
||||||
|
// - boundElements which contains only diff)
|
||||||
|
// - version & versionNonce, if we don't want to return to previous versions
|
||||||
|
for (const key of Object.keys(delta.inserted) as Array<
|
||||||
|
keyof typeof delta.inserted
|
||||||
|
>) {
|
||||||
|
if (key === "boundElements") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.excludedProperties.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = delta.inserted[key];
|
||||||
|
Reflect.set(directlyApplicablePartial, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
delta.deleted.boundElements?.length ||
|
delta.deleted.boundElements?.length ||
|
||||||
@ -1331,19 +1484,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this looks wrong, shouldn't be here
|
|
||||||
if (element.type === "image") {
|
|
||||||
const _delta = delta as Delta<ElementPartial<ExcalidrawImageElement>>;
|
|
||||||
// we want to override `crop` only if modified so that we don't reset
|
|
||||||
// when undoing/redoing unrelated change
|
|
||||||
if (_delta.deleted.crop || _delta.inserted.crop) {
|
|
||||||
Object.assign(directlyApplicablePartial, {
|
|
||||||
// apply change verbatim
|
|
||||||
crop: _delta.inserted.crop ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flags.containsVisibleDifference) {
|
if (!flags.containsVisibleDifference) {
|
||||||
// strip away fractional index, as even if it would be different, it doesn't have to result in visible change
|
// strip away fractional index, as even if it would be different, it doesn't have to result in visible change
|
||||||
const { index, ...rest } = directlyApplicablePartial;
|
const { index, ...rest } = directlyApplicablePartial;
|
||||||
@ -1650,6 +1790,29 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
): [ElementPartial, ElementPartial] {
|
): [ElementPartial, ElementPartial] {
|
||||||
try {
|
try {
|
||||||
Delta.diffArrays(deleted, inserted, "boundElements", (x) => x.id);
|
Delta.diffArrays(deleted, inserted, "boundElements", (x) => x.id);
|
||||||
|
|
||||||
|
// don't diff the points as:
|
||||||
|
// - we can't ensure the multiplayer order consistency without fractional index on each point
|
||||||
|
// - we prefer to not merge the points, as it might just lead to unexpected / incosistent results
|
||||||
|
const deletedPoints =
|
||||||
|
(
|
||||||
|
deleted as ElementPartial<
|
||||||
|
ExcalidrawFreeDrawElement | ExcalidrawLinearElement
|
||||||
|
>
|
||||||
|
).points ?? [];
|
||||||
|
|
||||||
|
const insertedPoints =
|
||||||
|
(
|
||||||
|
inserted as ElementPartial<
|
||||||
|
ExcalidrawFreeDrawElement | ExcalidrawLinearElement
|
||||||
|
>
|
||||||
|
).points ?? [];
|
||||||
|
|
||||||
|
if (!Delta.isDifferent(deletedPoints, insertedPoints)) {
|
||||||
|
// delete the points from delta if there is no difference, otherwise leave them as they were captured due to consistency
|
||||||
|
Reflect.deleteProperty(deleted, "points");
|
||||||
|
Reflect.deleteProperty(inserted, "points");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if postprocessing fails, it does not make sense to bubble up, but let's make sure we know about it
|
// if postprocessing fails, it does not make sense to bubble up, but let's make sure we know about it
|
||||||
console.error(`Couldn't postprocess elements delta.`);
|
console.error(`Couldn't postprocess elements delta.`);
|
||||||
@ -1665,7 +1828,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private static stripIrrelevantProps(
|
private static stripIrrelevantProps(
|
||||||
partial: Partial<OrderedExcalidrawElement>,
|
partial: Partial<OrderedExcalidrawElement>,
|
||||||
): ElementPartial {
|
): ElementPartial {
|
||||||
const { id, updated, version, versionNonce, ...strippedPartial } = partial;
|
const { id, updated, ...strippedPartial } = partial;
|
||||||
|
|
||||||
return strippedPartial;
|
return strippedPartial;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { generateNKeysBetween } from "fractional-indexing";
|
|||||||
|
|
||||||
import { arrayToMap } from "@excalidraw/common";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { hasBoundTextElement } from "./typeChecks";
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ import type {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class InvalidFractionalIndexError extends Error {
|
export class InvalidFractionalIndexError extends Error {
|
||||||
@ -161,9 +162,15 @@ export const syncMovedIndices = (
|
|||||||
|
|
||||||
// try generatating indices, throws on invalid movedElements
|
// try generatating indices, throws on invalid movedElements
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
const elementsCandidates = elements.map((x) =>
|
const elementsCandidates = elements.map((x) => {
|
||||||
elementsUpdates.has(x) ? { ...x, ...elementsUpdates.get(x) } : x,
|
const elementUpdates = elementsUpdates.get(x);
|
||||||
);
|
|
||||||
|
if (elementUpdates) {
|
||||||
|
return { ...x, index: elementUpdates.index };
|
||||||
|
}
|
||||||
|
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
|
||||||
// ensure next indices are valid before mutation, throws on invalid ones
|
// ensure next indices are valid before mutation, throws on invalid ones
|
||||||
validateFractionalIndices(
|
validateFractionalIndices(
|
||||||
@ -177,8 +184,8 @@ export const syncMovedIndices = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// split mutation so we don't end up in an incosistent state
|
// split mutation so we don't end up in an incosistent state
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
mutateElement(element, elementsMap, update);
|
mutateElement(element, elementsMap, { index });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback to default sync
|
// fallback to default sync
|
||||||
@ -189,7 +196,7 @@ export const syncMovedIndices = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes all invalid fractional indices with the array order by mutating passed elements.
|
* Synchronizes all invalid fractional indices within the array order by mutating elements in the passed array.
|
||||||
*
|
*
|
||||||
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
||||||
*/
|
*/
|
||||||
@ -200,13 +207,32 @@ export const syncInvalidIndices = (
|
|||||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
mutateElement(element, elementsMap, update);
|
mutateElement(element, elementsMap, { index });
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements as OrderedExcalidrawElement[];
|
return elements as OrderedExcalidrawElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes all invalid fractional indices within the array order by creating new instances of elements with corrected indices.
|
||||||
|
*
|
||||||
|
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
||||||
|
*/
|
||||||
|
export const syncInvalidIndicesImmutable = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): SceneElementsMap | undefined => {
|
||||||
|
const syncedElements = arrayToMap(elements);
|
||||||
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
|
syncedElements.set(element.id, newElementWith(element, { index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncedElements as SceneElementsMap;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get contiguous groups of indices of passed moved elements.
|
* Get contiguous groups of indices of passed moved elements.
|
||||||
*
|
*
|
||||||
|
@ -23,7 +23,7 @@ import type {
|
|||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
"id" | "version" | "versionNonce" | "updated"
|
"id" | "updated"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,8 +137,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
element.version++;
|
element.version = updates.version ?? element.version + 1;
|
||||||
element.versionNonce = randomInteger();
|
element.versionNonce = updates.versionNonce ?? randomInteger();
|
||||||
element.updated = getUpdatedTimestamp();
|
element.updated = getUpdatedTimestamp();
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
@ -172,9 +172,9 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
...updates,
|
...updates,
|
||||||
|
version: updates.version ?? element.version + 1,
|
||||||
|
versionNonce: updates.versionNonce ?? randomInteger(),
|
||||||
updated: getUpdatedTimestamp(),
|
updated: getUpdatedTimestamp(),
|
||||||
version: element.version + 1,
|
|
||||||
versionNonce: randomInteger(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,9 +19,17 @@ import { newElementWith } from "./mutateElement";
|
|||||||
|
|
||||||
import { ElementsDelta, AppStateDelta, Delta } from "./delta";
|
import { ElementsDelta, AppStateDelta, Delta } from "./delta";
|
||||||
|
|
||||||
import { hashElementsVersion, hashString } from "./index";
|
import {
|
||||||
|
syncInvalidIndicesImmutable,
|
||||||
|
hashElementsVersion,
|
||||||
|
hashString,
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
import type { OrderedExcalidrawElement, SceneElementsMap } from "./types";
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
SceneElementsMap,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const CaptureUpdateAction = {
|
export const CaptureUpdateAction = {
|
||||||
/**
|
/**
|
||||||
@ -105,7 +113,7 @@ export class Store {
|
|||||||
params:
|
params:
|
||||||
| {
|
| {
|
||||||
action: CaptureUpdateActionType;
|
action: CaptureUpdateActionType;
|
||||||
elements: SceneElementsMap | undefined;
|
elements: readonly ExcalidrawElement[] | undefined;
|
||||||
appState: AppState | ObservedAppState | undefined;
|
appState: AppState | ObservedAppState | undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@ -133,9 +141,15 @@ export class Store {
|
|||||||
this.app.scene.getElementsMapIncludingDeleted(),
|
this.app.scene.getElementsMapIncludingDeleted(),
|
||||||
this.app.state,
|
this.app.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduledSnapshot = currentSnapshot.maybeClone(
|
const scheduledSnapshot = currentSnapshot.maybeClone(
|
||||||
action,
|
action,
|
||||||
params.elements,
|
// let's sync invalid indices first, so that we could detect this change
|
||||||
|
// also have the synced elements immutable, so that we don't mutate elements,
|
||||||
|
// that are already in the scene, otherwise we wouldn't see any change
|
||||||
|
params.elements
|
||||||
|
? syncInvalidIndicesImmutable(params.elements)
|
||||||
|
: undefined,
|
||||||
params.appState,
|
params.appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -213,16 +227,7 @@ export class Store {
|
|||||||
// using the same instance, since in history we have a check against `HistoryEntry`, so that we don't re-record the same delta again
|
// using the same instance, since in history we have a check against `HistoryEntry`, so that we don't re-record the same delta again
|
||||||
storeDelta = delta;
|
storeDelta = delta;
|
||||||
} else {
|
} else {
|
||||||
// calculate the deltas based on the previous and next snapshot
|
storeDelta = StoreDelta.calculate(prevSnapshot, snapshot);
|
||||||
const elementsDelta = snapshot.metadata.didElementsChange
|
|
||||||
? ElementsDelta.calculate(prevSnapshot.elements, snapshot.elements)
|
|
||||||
: ElementsDelta.empty();
|
|
||||||
|
|
||||||
const appStateDelta = snapshot.metadata.didAppStateChange
|
|
||||||
? AppStateDelta.calculate(prevSnapshot.appState, snapshot.appState)
|
|
||||||
: AppStateDelta.empty();
|
|
||||||
|
|
||||||
storeDelta = StoreDelta.create(elementsDelta, appStateDelta);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!storeDelta.isEmpty()) {
|
if (!storeDelta.isEmpty()) {
|
||||||
@ -505,6 +510,24 @@ export class StoreDelta {
|
|||||||
return new this(opts.id, elements, appState);
|
return new this(opts.id, elements, appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the delta between the previous and next snapshot.
|
||||||
|
*/
|
||||||
|
public static calculate(
|
||||||
|
prevSnapshot: StoreSnapshot,
|
||||||
|
nextSnapshot: StoreSnapshot,
|
||||||
|
) {
|
||||||
|
const elementsDelta = nextSnapshot.metadata.didElementsChange
|
||||||
|
? ElementsDelta.calculate(prevSnapshot.elements, nextSnapshot.elements)
|
||||||
|
: ElementsDelta.empty();
|
||||||
|
|
||||||
|
const appStateDelta = nextSnapshot.metadata.didAppStateChange
|
||||||
|
? AppStateDelta.calculate(prevSnapshot.appState, nextSnapshot.appState)
|
||||||
|
: AppStateDelta.empty();
|
||||||
|
|
||||||
|
return this.create(elementsDelta, appStateDelta);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore a store delta instance from a DTO.
|
* Restore a store delta instance from a DTO.
|
||||||
*/
|
*/
|
||||||
@ -524,9 +547,7 @@ export class StoreDelta {
|
|||||||
id,
|
id,
|
||||||
elements: { added, removed, updated },
|
elements: { added, removed, updated },
|
||||||
}: DTO<StoreDelta>) {
|
}: DTO<StoreDelta>) {
|
||||||
const elements = ElementsDelta.create(added, removed, updated, {
|
const elements = ElementsDelta.create(added, removed, updated);
|
||||||
shouldRedistribute: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new this(id, elements, AppStateDelta.empty());
|
return new this(id, elements, AppStateDelta.empty());
|
||||||
}
|
}
|
||||||
@ -534,27 +555,10 @@ export class StoreDelta {
|
|||||||
/**
|
/**
|
||||||
* Inverse store delta, creates new instance of `StoreDelta`.
|
* Inverse store delta, creates new instance of `StoreDelta`.
|
||||||
*/
|
*/
|
||||||
public static inverse(delta: StoreDelta): StoreDelta {
|
public static inverse(delta: StoreDelta) {
|
||||||
return this.create(delta.elements.inverse(), delta.appState.inverse());
|
return this.create(delta.elements.inverse(), delta.appState.inverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply latest (remote) changes to the delta, creates new instance of `StoreDelta`.
|
|
||||||
*/
|
|
||||||
public static applyLatestChanges(
|
|
||||||
delta: StoreDelta,
|
|
||||||
elements: SceneElementsMap,
|
|
||||||
modifierOptions: "deleted" | "inserted",
|
|
||||||
): StoreDelta {
|
|
||||||
return this.create(
|
|
||||||
delta.elements.applyLatestChanges(elements, modifierOptions),
|
|
||||||
delta.appState,
|
|
||||||
{
|
|
||||||
id: delta.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the delta to the passed elements and appState, does not modify the snapshot.
|
* Apply the delta to the passed elements and appState, does not modify the snapshot.
|
||||||
*/
|
*/
|
||||||
@ -562,12 +566,9 @@ export class StoreDelta {
|
|||||||
delta: StoreDelta,
|
delta: StoreDelta,
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
prevSnapshot: StoreSnapshot = StoreSnapshot.empty(),
|
|
||||||
): [SceneElementsMap, AppState, boolean] {
|
): [SceneElementsMap, AppState, boolean] {
|
||||||
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
const [nextElements, elementsContainVisibleChange] =
|
||||||
elements,
|
delta.elements.applyTo(elements);
|
||||||
prevSnapshot.elements,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [nextAppState, appStateContainsVisibleChange] =
|
const [nextAppState, appStateContainsVisibleChange] =
|
||||||
delta.appState.applyTo(appState, nextElements);
|
delta.appState.applyTo(appState, nextElements);
|
||||||
@ -578,6 +579,28 @@ export class StoreDelta {
|
|||||||
return [nextElements, nextAppState, appliedVisibleChanges];
|
return [nextElements, nextAppState, appliedVisibleChanges];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply latest (remote) changes to the delta, creates new instance of `StoreDelta`.
|
||||||
|
*/
|
||||||
|
public static applyLatestChanges(
|
||||||
|
delta: StoreDelta,
|
||||||
|
prevElements: SceneElementsMap,
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
modifierOptions?: "deleted" | "inserted",
|
||||||
|
): StoreDelta {
|
||||||
|
return this.create(
|
||||||
|
delta.elements.applyLatestChanges(
|
||||||
|
prevElements,
|
||||||
|
nextElements,
|
||||||
|
modifierOptions,
|
||||||
|
),
|
||||||
|
delta.appState,
|
||||||
|
{
|
||||||
|
id: delta.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public isEmpty() {
|
public isEmpty() {
|
||||||
return this.elements.isEmpty() && this.appState.isEmpty();
|
return this.elements.isEmpty() && this.appState.isEmpty();
|
||||||
}
|
}
|
||||||
@ -687,11 +710,10 @@ export class StoreSnapshot {
|
|||||||
nextElements.set(id, changedElement);
|
nextElements.set(id, changedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextAppState = Object.assign(
|
const nextAppState = getObservedAppState({
|
||||||
{},
|
...this.appState,
|
||||||
this.appState,
|
...change.appState,
|
||||||
change.appState,
|
});
|
||||||
) as ObservedAppState;
|
|
||||||
|
|
||||||
return StoreSnapshot.create(nextElements, nextAppState, {
|
return StoreSnapshot.create(nextElements, nextAppState, {
|
||||||
// by default we assume that change is different from what we have in the snapshot
|
// by default we assume that change is different from what we have in the snapshot
|
||||||
@ -944,18 +966,26 @@ const getDefaultObservedAppState = (): ObservedAppState => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getObservedAppState = (appState: AppState): ObservedAppState => {
|
export const getObservedAppState = (
|
||||||
|
appState: AppState | ObservedAppState,
|
||||||
|
): ObservedAppState => {
|
||||||
const observedAppState = {
|
const observedAppState = {
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
selectedGroupIds: appState.selectedGroupIds,
|
selectedGroupIds: appState.selectedGroupIds,
|
||||||
editingLinearElementId: appState.editingLinearElement?.elementId || null,
|
|
||||||
selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
|
|
||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
activeLockedId: appState.activeLockedId,
|
activeLockedId: appState.activeLockedId,
|
||||||
lockedMultiSelections: appState.lockedMultiSelections,
|
lockedMultiSelections: appState.lockedMultiSelections,
|
||||||
|
editingLinearElementId:
|
||||||
|
(appState as AppState).editingLinearElement?.elementId ?? // prefer app state, as it's likely newer
|
||||||
|
(appState as ObservedAppState).editingLinearElementId ?? // fallback to observed app state, as it's likely older coming from a previous snapshot
|
||||||
|
null,
|
||||||
|
selectedLinearElementId:
|
||||||
|
(appState as AppState).selectedLinearElement?.elementId ??
|
||||||
|
(appState as ObservedAppState).selectedLinearElementId ??
|
||||||
|
null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
||||||
|
@ -505,8 +505,6 @@ describe("group-related duplication", () => {
|
|||||||
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(h.elements);
|
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ id: frame.id },
|
{ id: frame.id },
|
||||||
{ id: rectangle1.id, frameId: frame.id },
|
{ id: rectangle1.id, frameId: frame.id },
|
||||||
|
@ -103,6 +103,7 @@ import {
|
|||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getObservedAppState,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
@ -260,7 +261,6 @@ import type {
|
|||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
SceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
@ -702,6 +702,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
resetScene: this.resetScene,
|
resetScene: this.resetScene,
|
||||||
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
|
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
|
||||||
|
getSceneElementsMapIncludingDeleted:
|
||||||
|
this.getSceneElementsMapIncludingDeleted,
|
||||||
history: {
|
history: {
|
||||||
clear: this.resetHistory,
|
clear: this.resetHistory,
|
||||||
},
|
},
|
||||||
@ -3909,22 +3911,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}) => {
|
}) => {
|
||||||
const { elements, appState, collaborators, captureUpdate } = sceneData;
|
const { elements, appState, collaborators, captureUpdate } = sceneData;
|
||||||
|
|
||||||
const nextElements = elements ? syncInvalidIndices(elements) : undefined;
|
|
||||||
|
|
||||||
if (captureUpdate) {
|
if (captureUpdate) {
|
||||||
const nextElementsMap = elements
|
const observedAppState = appState
|
||||||
? (arrayToMap(nextElements ?? []) as SceneElementsMap)
|
? getObservedAppState({
|
||||||
: undefined;
|
...this.store.snapshot.appState,
|
||||||
|
...appState,
|
||||||
const nextAppState = appState
|
})
|
||||||
? // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState`
|
|
||||||
Object.assign({}, this.store.snapshot.appState, appState)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
this.store.scheduleMicroAction({
|
this.store.scheduleMicroAction({
|
||||||
action: captureUpdate,
|
action: captureUpdate,
|
||||||
elements: nextElementsMap,
|
elements: elements ?? [],
|
||||||
appState: nextAppState,
|
appState: observedAppState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3932,8 +3930,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState(appState);
|
this.setState(appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextElements) {
|
if (elements) {
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collaborators) {
|
if (collaborators) {
|
||||||
@ -10550,7 +10548,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// otherwise we would end up with duplicated fractional indices on undo
|
// otherwise we would end up with duplicated fractional indices on undo
|
||||||
this.store.scheduleMicroAction({
|
this.store.scheduleMicroAction({
|
||||||
action: CaptureUpdateAction.NEVER,
|
action: CaptureUpdateAction.NEVER,
|
||||||
elements: arrayToMap(elements) as SceneElementsMap,
|
elements,
|
||||||
appState: undefined,
|
appState: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,14 +4,81 @@ import {
|
|||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
StoreChange,
|
StoreChange,
|
||||||
StoreDelta,
|
StoreDelta,
|
||||||
type Store,
|
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { StoreSnapshot, Store } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { AppState } from "./types";
|
import type { AppState } from "./types";
|
||||||
|
|
||||||
class HistoryEntry extends StoreDelta {}
|
export class HistoryDelta extends StoreDelta {
|
||||||
|
/**
|
||||||
|
* Apply the delta to the passed elements and appState, does not modify the snapshot.
|
||||||
|
*/
|
||||||
|
public applyTo(
|
||||||
|
elements: SceneElementsMap,
|
||||||
|
appState: AppState,
|
||||||
|
snapshot: StoreSnapshot,
|
||||||
|
): [SceneElementsMap, AppState, boolean] {
|
||||||
|
const [nextElements, elementsContainVisibleChange] = this.elements.applyTo(
|
||||||
|
elements,
|
||||||
|
// used to fallback into local snapshot in case we couldn't apply the delta
|
||||||
|
// due to a missing (force deleted) elements in the scene
|
||||||
|
snapshot.elements,
|
||||||
|
// we don't want to apply the `version` and `versionNonce` properties for history
|
||||||
|
// as we always need to end up with a new version due to collaboration,
|
||||||
|
// approaching each undo / redo as a new user action
|
||||||
|
{
|
||||||
|
excludedProperties: new Set(["version", "versionNonce"]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [nextAppState, appStateContainsVisibleChange] = this.appState.applyTo(
|
||||||
|
appState,
|
||||||
|
nextElements,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appliedVisibleChanges =
|
||||||
|
elementsContainVisibleChange || appStateContainsVisibleChange;
|
||||||
|
|
||||||
|
return [nextElements, nextAppState, appliedVisibleChanges];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriding once to avoid type casting everywhere.
|
||||||
|
*/
|
||||||
|
public static override calculate(
|
||||||
|
prevSnapshot: StoreSnapshot,
|
||||||
|
nextSnapshot: StoreSnapshot,
|
||||||
|
) {
|
||||||
|
return super.calculate(prevSnapshot, nextSnapshot) as HistoryDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriding once to avoid type casting everywhere.
|
||||||
|
*/
|
||||||
|
public static override inverse(delta: StoreDelta): HistoryDelta {
|
||||||
|
return super.inverse(delta) as HistoryDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriding once to avoid type casting everywhere.
|
||||||
|
*/
|
||||||
|
public static override applyLatestChanges(
|
||||||
|
delta: StoreDelta,
|
||||||
|
prevElements: SceneElementsMap,
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
modifierOptions?: "deleted" | "inserted",
|
||||||
|
) {
|
||||||
|
return super.applyLatestChanges(
|
||||||
|
delta,
|
||||||
|
prevElements,
|
||||||
|
nextElements,
|
||||||
|
modifierOptions,
|
||||||
|
) as HistoryDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class HistoryChangedEvent {
|
export class HistoryChangedEvent {
|
||||||
constructor(
|
constructor(
|
||||||
@ -25,8 +92,8 @@ export class History {
|
|||||||
[HistoryChangedEvent]
|
[HistoryChangedEvent]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
public readonly undoStack: HistoryEntry[] = [];
|
public readonly undoStack: HistoryDelta[] = [];
|
||||||
public readonly redoStack: HistoryEntry[] = [];
|
public readonly redoStack: HistoryDelta[] = [];
|
||||||
|
|
||||||
public get isUndoStackEmpty() {
|
public get isUndoStackEmpty() {
|
||||||
return this.undoStack.length === 0;
|
return this.undoStack.length === 0;
|
||||||
@ -48,16 +115,16 @@ export class History {
|
|||||||
* Do not re-record history entries, which were already pushed to undo / redo stack, as part of history action.
|
* Do not re-record history entries, which were already pushed to undo / redo stack, as part of history action.
|
||||||
*/
|
*/
|
||||||
public record(delta: StoreDelta) {
|
public record(delta: StoreDelta) {
|
||||||
if (delta.isEmpty() || delta instanceof HistoryEntry) {
|
if (delta.isEmpty() || delta instanceof HistoryDelta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct history entry, so once it's emitted, it's not recorded again
|
// construct history entry, so once it's emitted, it's not recorded again
|
||||||
const entry = HistoryEntry.inverse(delta);
|
const historyDelta = HistoryDelta.inverse(delta);
|
||||||
|
|
||||||
this.undoStack.push(entry);
|
this.undoStack.push(historyDelta);
|
||||||
|
|
||||||
if (!entry.elements.isEmpty()) {
|
if (!historyDelta.elements.isEmpty()) {
|
||||||
// don't reset redo stack on local appState changes,
|
// don't reset redo stack on local appState changes,
|
||||||
// as a simple click (unselect) could lead to losing all the redo entries
|
// as a simple click (unselect) could lead to losing all the redo entries
|
||||||
// only reset on non empty elements changes!
|
// only reset on non empty elements changes!
|
||||||
@ -74,7 +141,7 @@ export class History {
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
() => History.pop(this.undoStack),
|
() => History.pop(this.undoStack),
|
||||||
(entry: HistoryEntry) => History.push(this.redoStack, entry, elements),
|
(entry: HistoryDelta) => History.push(this.redoStack, entry),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,20 +150,20 @@ export class History {
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
() => History.pop(this.redoStack),
|
() => History.pop(this.redoStack),
|
||||||
(entry: HistoryEntry) => History.push(this.undoStack, entry, elements),
|
(entry: HistoryDelta) => History.push(this.undoStack, entry),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private perform(
|
private perform(
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pop: () => HistoryEntry | null,
|
pop: () => HistoryDelta | null,
|
||||||
push: (entry: HistoryEntry) => void,
|
push: (entry: HistoryDelta) => void,
|
||||||
): [SceneElementsMap, AppState] | void {
|
): [SceneElementsMap, AppState] | void {
|
||||||
try {
|
try {
|
||||||
let historyEntry = pop();
|
let historyDelta = pop();
|
||||||
|
|
||||||
if (historyEntry === null) {
|
if (historyDelta === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,41 +175,47 @@ export class History {
|
|||||||
let nextAppState = appState;
|
let nextAppState = appState;
|
||||||
let containsVisibleChange = false;
|
let containsVisibleChange = false;
|
||||||
|
|
||||||
// iterate through the history entries in case they result in no visible changes
|
// iterate through the history entries in case ;they result in no visible changes
|
||||||
while (historyEntry) {
|
while (historyDelta) {
|
||||||
try {
|
try {
|
||||||
[nextElements, nextAppState, containsVisibleChange] =
|
[nextElements, nextAppState, containsVisibleChange] =
|
||||||
StoreDelta.applyTo(
|
historyDelta.applyTo(nextElements, nextAppState, prevSnapshot);
|
||||||
historyEntry,
|
|
||||||
nextElements,
|
|
||||||
nextAppState,
|
|
||||||
prevSnapshot,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const prevElements = prevSnapshot.elements;
|
||||||
const nextSnapshot = prevSnapshot.maybeClone(
|
const nextSnapshot = prevSnapshot.maybeClone(
|
||||||
action,
|
action,
|
||||||
nextElements,
|
nextElements,
|
||||||
nextAppState,
|
nextAppState,
|
||||||
);
|
);
|
||||||
|
|
||||||
// schedule immediate capture, so that it's emitted for the sync purposes
|
const change = StoreChange.create(prevSnapshot, nextSnapshot);
|
||||||
this.store.scheduleMicroAction({
|
const delta = HistoryDelta.applyLatestChanges(
|
||||||
action,
|
historyDelta,
|
||||||
change: StoreChange.create(prevSnapshot, nextSnapshot),
|
prevElements,
|
||||||
delta: historyEntry,
|
nextElements,
|
||||||
});
|
);
|
||||||
|
|
||||||
|
if (!delta.isEmpty()) {
|
||||||
|
// schedule immediate capture, so that it's emitted for the sync purposes
|
||||||
|
this.store.scheduleMicroAction({
|
||||||
|
action,
|
||||||
|
change,
|
||||||
|
delta,
|
||||||
|
});
|
||||||
|
|
||||||
|
historyDelta = delta;
|
||||||
|
}
|
||||||
|
|
||||||
prevSnapshot = nextSnapshot;
|
prevSnapshot = nextSnapshot;
|
||||||
} finally {
|
} finally {
|
||||||
// make sure to always push, even if the delta is corrupted
|
push(historyDelta);
|
||||||
push(historyEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containsVisibleChange) {
|
if (containsVisibleChange) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
historyEntry = pop();
|
historyDelta = pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
return [nextElements, nextAppState];
|
return [nextElements, nextAppState];
|
||||||
@ -155,7 +228,7 @@ export class History {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static pop(stack: HistoryEntry[]): HistoryEntry | null {
|
private static pop(stack: HistoryDelta[]): HistoryDelta | null {
|
||||||
if (!stack.length) {
|
if (!stack.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -169,18 +242,8 @@ export class History {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static push(
|
private static push(stack: HistoryDelta[], entry: HistoryDelta) {
|
||||||
stack: HistoryEntry[],
|
const inversedEntry = HistoryDelta.inverse(entry);
|
||||||
entry: HistoryEntry,
|
return stack.push(inversedEntry);
|
||||||
prevElements: SceneElementsMap,
|
|
||||||
) {
|
|
||||||
const inversedEntry = HistoryEntry.inverse(entry);
|
|
||||||
const updatedEntry = HistoryEntry.applyLatestChanges(
|
|
||||||
inversedEntry,
|
|
||||||
prevElements,
|
|
||||||
"inserted",
|
|
||||||
);
|
|
||||||
|
|
||||||
return stack.push(updatedEntry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1269,12 +1269,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1420,14 +1422,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1014066025,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1505387817,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -1459,7 +1461,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -1511,12 +1513,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1563,12 +1567,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1598,9 +1604,11 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"id0": {
|
"id0": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1745,14 +1753,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1014066025,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1505387817,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -1784,7 +1792,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -1836,12 +1844,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1888,12 +1898,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1923,9 +1935,11 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"id0": {
|
"id0": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2131,12 +2145,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2287,7 +2303,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1116226695,
|
"versionNonce": 1014066025,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@ -2339,12 +2355,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2370,9 +2388,11 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"id0": {
|
"id0": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2551,14 +2571,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1014066025,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 1604849351,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -2610,12 +2630,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2662,12 +2684,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 5,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2827,7 +2851,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 493213705,
|
"versionNonce": 81784553,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -2854,14 +2878,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1014066025,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 747212839,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -2913,12 +2937,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2965,12 +2991,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3020,9 +3048,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id3": {
|
"id3": {
|
||||||
@ -3030,9 +3060,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3186,7 +3218,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 908564423,
|
"versionNonce": 1359939303,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -3211,14 +3243,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1315507081,
|
"seed": 640725609,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 406373543,
|
"versionNonce": 908564423,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -3270,12 +3302,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3322,12 +3356,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3349,9 +3385,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3372,9 +3410,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"backgroundColor": "#a5d8ff",
|
"backgroundColor": "#a5d8ff",
|
||||||
|
"version": 5,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3395,9 +3435,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"fillStyle": "cross-hatch",
|
"fillStyle": "cross-hatch",
|
||||||
|
"version": 6,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
"version": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3418,9 +3460,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
|
"version": 7,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
"version": 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3441,9 +3485,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
|
"version": 8,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
"version": 7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3464,9 +3510,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
|
"version": 9,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
"version": 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3500,6 +3548,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
@ -3508,6 +3557,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3652,14 +3702,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1150084233,
|
"seed": 1116226695,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 23633383,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -3743,12 +3793,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3795,12 +3847,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3822,9 +3876,11 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"index": "Zz",
|
"index": "Zz",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3969,14 +4025,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1014066025,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -4060,12 +4116,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4112,12 +4170,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4139,9 +4199,11 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"index": "Zz",
|
"index": "Zz",
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4296,7 +4358,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1723083209,
|
"versionNonce": 1006504105,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -4321,14 +4383,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 400692809,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 760410951,
|
"versionNonce": 289600103,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -4380,12 +4442,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4432,12 +4496,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4487,9 +4553,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id3": {
|
"id3": {
|
||||||
@ -4497,9 +4565,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -4526,21 +4596,25 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"id0": {
|
"id0": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 5,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id3": {
|
"id3": {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 5,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id9",
|
"id9",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -5594,14 +5668,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1604849351,
|
"seed": 1505387817,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 493213705,
|
"versionNonce": 915032327,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -5653,12 +5727,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -5705,12 +5781,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -6786,7 +6864,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 81784553,
|
"versionNonce": 1723083209,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -6813,14 +6891,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 400692809,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 747212839,
|
"versionNonce": 760410951,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -6872,12 +6950,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -6924,12 +7004,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -7001,9 +7083,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id12",
|
"id12",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id3": {
|
"id3": {
|
||||||
@ -7011,9 +7095,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id12",
|
"id12",
|
||||||
],
|
],
|
||||||
|
"version": 4,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
"version": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -9822,12 +9908,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
"version": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1505387817,
|
"versionNonce": 23633383,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1604849351,
|
"seed": 1505387817,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 81784553,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
@ -89,7 +89,7 @@ exports[`move element > rectangle 5`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1116226695,
|
"versionNonce": 1014066025,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 40,
|
"y": 40,
|
||||||
@ -126,7 +126,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1723083209,
|
"versionNonce": 1006504105,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1150084233,
|
"seed": 1116226695,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1051383431,
|
"versionNonce": 1984422985,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@ -208,7 +208,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 1604849351,
|
"seed": 23633383,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
@ -221,7 +221,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1996028265,
|
"versionNonce": 1573789895,
|
||||||
"width": "81.00000",
|
"width": "81.00000",
|
||||||
"x": "110.00000",
|
"x": "110.00000",
|
||||||
"y": 50,
|
"y": 50,
|
||||||
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 1604849351,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -105,7 +105,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 1604849351,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -431,12 +431,17 @@ export const assertElements = <T extends AllPossibleKeys<ExcalidrawElement>>(
|
|||||||
expect(h.state.selectedElementIds).toEqual(selectedElementIds);
|
expect(h.state.selectedElementIds).toEqual(selectedElementIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stripSeed = (deltas: Record<string, { deleted: any; inserted: any }>) =>
|
const stripProps = (
|
||||||
|
deltas: Record<string, { deleted: any; inserted: any }>,
|
||||||
|
props: string[],
|
||||||
|
) =>
|
||||||
Object.entries(deltas).reduce((acc, curr) => {
|
Object.entries(deltas).reduce((acc, curr) => {
|
||||||
const { inserted, deleted, ...rest } = curr[1];
|
const { inserted, deleted, ...rest } = curr[1];
|
||||||
|
|
||||||
delete inserted.seed;
|
for (const prop of props) {
|
||||||
delete deleted.seed;
|
delete inserted[prop];
|
||||||
|
delete deleted[prop];
|
||||||
|
}
|
||||||
|
|
||||||
acc[curr[0]] = {
|
acc[curr[0]] = {
|
||||||
inserted,
|
inserted,
|
||||||
@ -453,9 +458,9 @@ export const checkpointHistory = (history: History, name: string) => {
|
|||||||
...x,
|
...x,
|
||||||
elements: {
|
elements: {
|
||||||
...x.elements,
|
...x.elements,
|
||||||
added: stripSeed(x.elements.added),
|
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
|
||||||
removed: stripSeed(x.elements.removed),
|
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
|
||||||
updated: stripSeed(x.elements.updated),
|
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
).toMatchSnapshot(`[${name}] undo stack`);
|
).toMatchSnapshot(`[${name}] undo stack`);
|
||||||
@ -465,9 +470,9 @@ export const checkpointHistory = (history: History, name: string) => {
|
|||||||
...x,
|
...x,
|
||||||
elements: {
|
elements: {
|
||||||
...x.elements,
|
...x.elements,
|
||||||
added: stripSeed(x.elements.added),
|
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
|
||||||
removed: stripSeed(x.elements.removed),
|
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
|
||||||
updated: stripSeed(x.elements.updated),
|
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
).toMatchSnapshot(`[${name}] redo stack`);
|
).toMatchSnapshot(`[${name}] redo stack`);
|
||||||
|
@ -813,6 +813,9 @@ export interface ExcalidrawImperativeAPI {
|
|||||||
getSceneElementsIncludingDeleted: InstanceType<
|
getSceneElementsIncludingDeleted: InstanceType<
|
||||||
typeof App
|
typeof App
|
||||||
>["getSceneElementsIncludingDeleted"];
|
>["getSceneElementsIncludingDeleted"];
|
||||||
|
getSceneElementsMapIncludingDeleted: InstanceType<
|
||||||
|
typeof App
|
||||||
|
>["getSceneElementsMapIncludingDeleted"];
|
||||||
history: {
|
history: {
|
||||||
clear: InstanceType<typeof App>["resetHistory"];
|
clear: InstanceType<typeof App>["resetHistory"];
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user