mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-29 04:10:02 +02:00
feat: apply deltas API (#9869)
This commit is contained in:
@@ -164,9 +164,14 @@ export class Scene {
|
||||
return this.frames;
|
||||
}
|
||||
|
||||
constructor(elements: ElementsMapOrArray | null = null) {
|
||||
constructor(
|
||||
elements: ElementsMapOrArray | null = null,
|
||||
options?: {
|
||||
skipValidation?: true;
|
||||
},
|
||||
) {
|
||||
if (elements) {
|
||||
this.replaceAllElements(elements);
|
||||
this.replaceAllElements(elements, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,12 +268,19 @@ export class Scene {
|
||||
return didChange;
|
||||
}
|
||||
|
||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||
replaceAllElements(
|
||||
nextElements: ElementsMapOrArray,
|
||||
options?: {
|
||||
skipValidation?: true;
|
||||
},
|
||||
) {
|
||||
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||
const _nextElements = toArray(nextElements);
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
|
||||
validateIndicesThrottled(_nextElements);
|
||||
if (!options?.skipValidation) {
|
||||
validateIndicesThrottled(_nextElements);
|
||||
}
|
||||
|
||||
this.elements = syncInvalidIndices(_nextElements);
|
||||
this.elementsMap.clear();
|
||||
|
@@ -55,10 +55,10 @@ import { getNonDeletedGroupIds } from "./groups";
|
||||
|
||||
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
||||
|
||||
import { Scene } from "./Scene";
|
||||
|
||||
import { StoreSnapshot } from "./store";
|
||||
|
||||
import { Scene } from "./Scene";
|
||||
|
||||
import type { BindableProp, BindingProp } from "./binding";
|
||||
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
@@ -153,10 +153,14 @@ export class Delta<T> {
|
||||
/**
|
||||
* Merges two deltas into a new one.
|
||||
*/
|
||||
public static merge<T>(delta1: Delta<T>, delta2: Delta<T>) {
|
||||
public static merge<T>(
|
||||
delta1: Delta<T>,
|
||||
delta2: Delta<T>,
|
||||
delta3: Delta<T> = Delta.empty(),
|
||||
) {
|
||||
return Delta.create(
|
||||
{ ...delta1.deleted, ...delta2.deleted },
|
||||
{ ...delta1.inserted, ...delta2.inserted },
|
||||
{ ...delta1.deleted, ...delta2.deleted, ...delta3.deleted },
|
||||
{ ...delta1.inserted, ...delta2.inserted, ...delta3.inserted },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +170,7 @@ export class Delta<T> {
|
||||
public static mergeObjects<T extends { [key: string]: unknown }>(
|
||||
prev: T,
|
||||
added: T,
|
||||
removed: T,
|
||||
removed: T = {} as T,
|
||||
) {
|
||||
const cloned = { ...prev };
|
||||
|
||||
@@ -520,6 +524,10 @@ export interface DeltaContainer<T> {
|
||||
export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
private constructor(public delta: Delta<ObservedAppState>) {}
|
||||
|
||||
public static create(delta: Delta<ObservedAppState>): AppStateDelta {
|
||||
return new AppStateDelta(delta);
|
||||
}
|
||||
|
||||
public static calculate<T extends ObservedAppState>(
|
||||
prevAppState: T,
|
||||
nextAppState: T,
|
||||
@@ -550,7 +558,74 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
}
|
||||
|
||||
public squash(delta: AppStateDelta): this {
|
||||
this.delta = Delta.merge(this.delta, delta.delta);
|
||||
if (delta.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const mergedDeletedSelectedElementIds = Delta.mergeObjects(
|
||||
this.delta.deleted.selectedElementIds ?? {},
|
||||
delta.delta.deleted.selectedElementIds ?? {},
|
||||
);
|
||||
|
||||
const mergedInsertedSelectedElementIds = Delta.mergeObjects(
|
||||
this.delta.inserted.selectedElementIds ?? {},
|
||||
delta.delta.inserted.selectedElementIds ?? {},
|
||||
);
|
||||
|
||||
const mergedDeletedSelectedGroupIds = Delta.mergeObjects(
|
||||
this.delta.deleted.selectedGroupIds ?? {},
|
||||
delta.delta.deleted.selectedGroupIds ?? {},
|
||||
);
|
||||
|
||||
const mergedInsertedSelectedGroupIds = Delta.mergeObjects(
|
||||
this.delta.inserted.selectedGroupIds ?? {},
|
||||
delta.delta.inserted.selectedGroupIds ?? {},
|
||||
);
|
||||
|
||||
const mergedDeletedLockedMultiSelections = Delta.mergeObjects(
|
||||
this.delta.deleted.lockedMultiSelections ?? {},
|
||||
delta.delta.deleted.lockedMultiSelections ?? {},
|
||||
);
|
||||
|
||||
const mergedInsertedLockedMultiSelections = Delta.mergeObjects(
|
||||
this.delta.inserted.lockedMultiSelections ?? {},
|
||||
delta.delta.inserted.lockedMultiSelections ?? {},
|
||||
);
|
||||
|
||||
const mergedInserted: Partial<ObservedAppState> = {};
|
||||
const mergedDeleted: Partial<ObservedAppState> = {};
|
||||
|
||||
if (
|
||||
Object.keys(mergedDeletedSelectedElementIds).length ||
|
||||
Object.keys(mergedInsertedSelectedElementIds).length
|
||||
) {
|
||||
mergedDeleted.selectedElementIds = mergedDeletedSelectedElementIds;
|
||||
mergedInserted.selectedElementIds = mergedInsertedSelectedElementIds;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(mergedDeletedSelectedGroupIds).length ||
|
||||
Object.keys(mergedInsertedSelectedGroupIds).length
|
||||
) {
|
||||
mergedDeleted.selectedGroupIds = mergedDeletedSelectedGroupIds;
|
||||
mergedInserted.selectedGroupIds = mergedInsertedSelectedGroupIds;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(mergedDeletedLockedMultiSelections).length ||
|
||||
Object.keys(mergedInsertedLockedMultiSelections).length
|
||||
) {
|
||||
mergedDeleted.lockedMultiSelections = mergedDeletedLockedMultiSelections;
|
||||
mergedInserted.lockedMultiSelections =
|
||||
mergedInsertedLockedMultiSelections;
|
||||
}
|
||||
|
||||
this.delta = Delta.merge(
|
||||
this.delta,
|
||||
delta.delta,
|
||||
Delta.create(mergedDeleted, mergedInserted),
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -562,11 +637,13 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
const {
|
||||
selectedElementIds: deletedSelectedElementIds = {},
|
||||
selectedGroupIds: deletedSelectedGroupIds = {},
|
||||
lockedMultiSelections: deletedLockedMultiSelections = {},
|
||||
} = this.delta.deleted;
|
||||
|
||||
const {
|
||||
selectedElementIds: insertedSelectedElementIds = {},
|
||||
selectedGroupIds: insertedSelectedGroupIds = {},
|
||||
lockedMultiSelections: insertedLockedMultiSelections = {},
|
||||
selectedLinearElement: insertedSelectedLinearElement,
|
||||
...directlyApplicablePartial
|
||||
} = this.delta.inserted;
|
||||
@@ -583,6 +660,12 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
deletedSelectedGroupIds,
|
||||
);
|
||||
|
||||
const mergedLockedMultiSelections = Delta.mergeObjects(
|
||||
appState.lockedMultiSelections,
|
||||
insertedLockedMultiSelections,
|
||||
deletedLockedMultiSelections,
|
||||
);
|
||||
|
||||
const selectedLinearElement =
|
||||
insertedSelectedLinearElement &&
|
||||
nextElements.has(insertedSelectedLinearElement.elementId)
|
||||
@@ -600,6 +683,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
...directlyApplicablePartial,
|
||||
selectedElementIds: mergedSelectedElementIds,
|
||||
selectedGroupIds: mergedSelectedGroupIds,
|
||||
lockedMultiSelections: mergedLockedMultiSelections,
|
||||
selectedLinearElement:
|
||||
typeof insertedSelectedLinearElement !== "undefined"
|
||||
? selectedLinearElement
|
||||
@@ -904,12 +988,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
"lockedMultiSelections",
|
||||
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
||||
);
|
||||
Delta.diffObjects(
|
||||
deleted,
|
||||
inserted,
|
||||
"activeLockedId",
|
||||
(prevValue) => (prevValue ?? null) as ValueOf<T["activeLockedId"]>,
|
||||
);
|
||||
} catch (e) {
|
||||
// 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 appstate change deltas.`);
|
||||
@@ -938,12 +1016,13 @@ type ElementPartial<TElement extends ExcalidrawElement = ExcalidrawElement> =
|
||||
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
||||
|
||||
export type ApplyToOptions = {
|
||||
excludedProperties: Set<keyof ElementPartial>;
|
||||
excludedProperties?: Set<keyof ElementPartial>;
|
||||
};
|
||||
|
||||
type ApplyToFlags = {
|
||||
containsVisibleDifference: boolean;
|
||||
containsZindexDifference: boolean;
|
||||
applyDirection: "forward" | "backward" | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1044,6 +1123,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
deleted.version !== inserted.version
|
||||
);
|
||||
|
||||
private static satisfiesUniqueInvariants = (
|
||||
elementsDelta: ElementsDelta,
|
||||
id: string,
|
||||
) => {
|
||||
const { added, removed, updated } = elementsDelta;
|
||||
// it's required that there is only one unique delta type per element
|
||||
return [added[id], removed[id], updated[id]].filter(Boolean).length === 1;
|
||||
};
|
||||
|
||||
private static validate(
|
||||
elementsDelta: ElementsDelta,
|
||||
type: "added" | "removed" | "updated",
|
||||
@@ -1052,6 +1140,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
||||
if (
|
||||
!this.satisfiesCommmonInvariants(delta) ||
|
||||
!this.satisfiesUniqueInvariants(elementsDelta, id) ||
|
||||
!satifiesSpecialInvariants(delta)
|
||||
) {
|
||||
console.error(
|
||||
@@ -1311,9 +1400,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
public applyTo(
|
||||
elements: SceneElementsMap,
|
||||
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
||||
options: ApplyToOptions = {
|
||||
excludedProperties: new Set(),
|
||||
},
|
||||
options?: ApplyToOptions,
|
||||
): [SceneElementsMap, boolean] {
|
||||
let nextElements = new Map(elements) as SceneElementsMap;
|
||||
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||
@@ -1321,22 +1408,28 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
const flags: ApplyToFlags = {
|
||||
containsVisibleDifference: false,
|
||||
containsZindexDifference: false,
|
||||
applyDirection: undefined,
|
||||
};
|
||||
|
||||
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
||||
try {
|
||||
const applyDeltas = ElementsDelta.createApplier(
|
||||
elements,
|
||||
nextElements,
|
||||
snapshot,
|
||||
options,
|
||||
flags,
|
||||
options,
|
||||
);
|
||||
|
||||
const addedElements = applyDeltas(this.added);
|
||||
const removedElements = applyDeltas(this.removed);
|
||||
const updatedElements = applyDeltas(this.updated);
|
||||
|
||||
const affectedElements = this.resolveConflicts(elements, nextElements);
|
||||
const affectedElements = this.resolveConflicts(
|
||||
elements,
|
||||
nextElements,
|
||||
flags.applyDirection,
|
||||
);
|
||||
|
||||
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
||||
changedElements = new Map([
|
||||
@@ -1360,22 +1453,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
}
|
||||
|
||||
try {
|
||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||
// the following reorder performs mutations, but only on new instances of changed elements,
|
||||
// unless something goes really bad and it fallbacks to fixing all invalid indices
|
||||
nextElements = ElementsDelta.reorderElements(
|
||||
nextElements,
|
||||
changedElements,
|
||||
flags,
|
||||
);
|
||||
|
||||
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||
// we also don't have a scene on the server
|
||||
// so we are creating a temp scene just to query and mutate elements
|
||||
const tempScene = new Scene(nextElements);
|
||||
|
||||
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||
// Need ordered nextElements to avoid z-index binding issues
|
||||
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
||||
ElementsDelta.redrawElements(nextElements, changedElements);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Couldn't mutate elements after applying elements change`,
|
||||
@@ -1391,47 +1477,112 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
}
|
||||
|
||||
public squash(delta: ElementsDelta): this {
|
||||
if (delta.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const { added, removed, updated } = delta;
|
||||
|
||||
const mergeBoundElements = (
|
||||
prevDelta: Delta<ElementPartial>,
|
||||
nextDelta: Delta<ElementPartial>,
|
||||
) => {
|
||||
const mergedDeletedBoundElements =
|
||||
Delta.mergeArrays(
|
||||
prevDelta.deleted.boundElements ?? [],
|
||||
nextDelta.deleted.boundElements ?? [],
|
||||
undefined,
|
||||
(x) => x.id,
|
||||
) ?? [];
|
||||
|
||||
const mergedInsertedBoundElements =
|
||||
Delta.mergeArrays(
|
||||
prevDelta.inserted.boundElements ?? [],
|
||||
nextDelta.inserted.boundElements ?? [],
|
||||
undefined,
|
||||
(x) => x.id,
|
||||
) ?? [];
|
||||
|
||||
if (
|
||||
!mergedDeletedBoundElements.length &&
|
||||
!mergedInsertedBoundElements.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Delta.create(
|
||||
{
|
||||
boundElements: mergedDeletedBoundElements,
|
||||
},
|
||||
{
|
||||
boundElements: mergedInsertedBoundElements,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(added)) {
|
||||
const prevDelta = this.added[id];
|
||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.added[id] = nextDelta;
|
||||
} else {
|
||||
this.added[id] = Delta.merge(prevDelta, nextDelta);
|
||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||
delete this.removed[id];
|
||||
delete this.updated[id];
|
||||
|
||||
this.added[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(removed)) {
|
||||
const prevDelta = this.removed[id];
|
||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.removed[id] = nextDelta;
|
||||
} else {
|
||||
this.removed[id] = Delta.merge(prevDelta, nextDelta);
|
||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||
delete this.added[id];
|
||||
delete this.updated[id];
|
||||
|
||||
this.removed[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(updated)) {
|
||||
const prevDelta = this.updated[id];
|
||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.updated[id] = nextDelta;
|
||||
} else {
|
||||
this.updated[id] = Delta.merge(prevDelta, nextDelta);
|
||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||
const updatedDelta = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||
|
||||
if (prevDelta === this.added[id]) {
|
||||
this.added[id] = updatedDelta;
|
||||
} else if (prevDelta === this.removed[id]) {
|
||||
this.removed[id] = updatedDelta;
|
||||
} else {
|
||||
this.updated[id] = updatedDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
ElementsDelta.validate(this, "added", ElementsDelta.satisfiesAddition);
|
||||
ElementsDelta.validate(this, "removed", ElementsDelta.satisfiesRemoval);
|
||||
ElementsDelta.validate(this, "updated", ElementsDelta.satisfiesUpdate);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static createApplier =
|
||||
(
|
||||
prevElements: SceneElementsMap,
|
||||
nextElements: SceneElementsMap,
|
||||
snapshot: StoreSnapshot["elements"],
|
||||
options: ApplyToOptions,
|
||||
flags: ApplyToFlags,
|
||||
options?: ApplyToOptions,
|
||||
) =>
|
||||
(deltas: Record<string, Delta<ElementPartial>>) => {
|
||||
const getElement = ElementsDelta.createGetter(
|
||||
@@ -1444,15 +1595,26 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
const element = getElement(id, delta.inserted);
|
||||
|
||||
if (element) {
|
||||
const newElement = ElementsDelta.applyDelta(
|
||||
const nextElement = ElementsDelta.applyDelta(
|
||||
element,
|
||||
delta,
|
||||
options,
|
||||
flags,
|
||||
options,
|
||||
);
|
||||
|
||||
nextElements.set(newElement.id, newElement);
|
||||
acc.set(newElement.id, newElement);
|
||||
nextElements.set(nextElement.id, nextElement);
|
||||
acc.set(nextElement.id, nextElement);
|
||||
|
||||
if (!flags.applyDirection) {
|
||||
const prevElement = prevElements.get(id);
|
||||
|
||||
if (prevElement) {
|
||||
flags.applyDirection =
|
||||
prevElement.version > nextElement.version
|
||||
? "backward"
|
||||
: "forward";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -1497,8 +1659,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
private static applyDelta(
|
||||
element: OrderedExcalidrawElement,
|
||||
delta: Delta<ElementPartial>,
|
||||
options: ApplyToOptions,
|
||||
flags: ApplyToFlags,
|
||||
options?: ApplyToOptions,
|
||||
) {
|
||||
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
||||
|
||||
@@ -1512,7 +1674,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.excludedProperties.has(key)) {
|
||||
if (options?.excludedProperties?.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1552,7 +1714,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
delta.deleted.index !== delta.inserted.index;
|
||||
}
|
||||
|
||||
return newElementWith(element, directlyApplicablePartial);
|
||||
return newElementWith(element, directlyApplicablePartial, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1592,6 +1754,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
private resolveConflicts(
|
||||
prevElements: SceneElementsMap,
|
||||
nextElements: SceneElementsMap,
|
||||
applyDirection: "forward" | "backward" = "forward",
|
||||
) {
|
||||
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
||||
const updater = (
|
||||
@@ -1603,21 +1766,36 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevElement = prevElements.get(element.id);
|
||||
const nextVersion =
|
||||
applyDirection === "forward"
|
||||
? nextElement.version + 1
|
||||
: nextElement.version - 1;
|
||||
|
||||
const elementUpdates = updates as ElementUpdate<OrderedExcalidrawElement>;
|
||||
|
||||
let affectedElement: OrderedExcalidrawElement;
|
||||
|
||||
if (prevElements.get(element.id) === nextElement) {
|
||||
if (prevElement === nextElement) {
|
||||
// create the new element instance in case we didn't modify the element yet
|
||||
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
||||
affectedElement = newElementWith(
|
||||
nextElement,
|
||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||
{
|
||||
...elementUpdates,
|
||||
version: nextVersion,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
affectedElement = mutateElement(
|
||||
nextElement,
|
||||
nextElements,
|
||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||
);
|
||||
affectedElement = mutateElement(nextElement, nextElements, {
|
||||
...elementUpdates,
|
||||
// don't modify the version further, if it's already different
|
||||
version:
|
||||
prevElement?.version !== nextElement.version
|
||||
? nextElement.version
|
||||
: nextVersion,
|
||||
});
|
||||
}
|
||||
|
||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||
@@ -1722,6 +1900,31 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
||||
}
|
||||
|
||||
public static redrawElements(
|
||||
nextElements: SceneElementsMap,
|
||||
changedElements: Map<string, OrderedExcalidrawElement>,
|
||||
) {
|
||||
try {
|
||||
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||
// we also don't have a scene on the server
|
||||
// so we are creating a temp scene just to query and mutate elements
|
||||
const tempScene = new Scene(nextElements, { skipValidation: true });
|
||||
|
||||
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||
|
||||
// needs ordered nextElements to avoid z-index binding issues
|
||||
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
||||
} catch (e) {
|
||||
console.error(`Couldn't redraw elements`, e);
|
||||
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
return nextElements;
|
||||
}
|
||||
}
|
||||
|
||||
private static redrawTextBoundingBoxes(
|
||||
scene: Scene,
|
||||
changed: Map<string, OrderedExcalidrawElement>,
|
||||
@@ -1776,6 +1979,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
) {
|
||||
for (const element of changed.values()) {
|
||||
if (!element.isDeleted && isBindableElement(element)) {
|
||||
// TODO: with precise bindings this is quite expensive, so consider optimisation so it's only triggered when the arrow does not intersect (imprecise) element bounds
|
||||
updateBoundElements(element, scene, {
|
||||
changedElements: changed,
|
||||
});
|
||||
|
@@ -552,10 +552,26 @@ export class StoreDelta {
|
||||
public static load({
|
||||
id,
|
||||
elements: { added, removed, updated },
|
||||
appState: { delta: appStateDelta },
|
||||
}: DTO<StoreDelta>) {
|
||||
const elements = ElementsDelta.create(added, removed, updated);
|
||||
const appState = AppStateDelta.create(appStateDelta);
|
||||
|
||||
return new this(id, elements, AppStateDelta.empty());
|
||||
return new this(id, elements, appState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Squash the passed deltas into the aggregated delta instance.
|
||||
*/
|
||||
public static squash(...deltas: StoreDelta[]) {
|
||||
const aggregatedDelta = StoreDelta.empty();
|
||||
|
||||
for (const delta of deltas) {
|
||||
aggregatedDelta.elements.squash(delta.elements);
|
||||
aggregatedDelta.appState.squash(delta.appState);
|
||||
}
|
||||
|
||||
return aggregatedDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -572,9 +588,7 @@ export class StoreDelta {
|
||||
delta: StoreDelta,
|
||||
elements: SceneElementsMap,
|
||||
appState: AppState,
|
||||
options: ApplyToOptions = {
|
||||
excludedProperties: new Set(),
|
||||
},
|
||||
options?: ApplyToOptions,
|
||||
): [SceneElementsMap, AppState, boolean] {
|
||||
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
||||
elements,
|
||||
@@ -613,6 +627,10 @@ export class StoreDelta {
|
||||
);
|
||||
}
|
||||
|
||||
public static empty() {
|
||||
return StoreDelta.create(ElementsDelta.empty(), AppStateDelta.empty());
|
||||
}
|
||||
|
||||
public isEmpty() {
|
||||
return this.elements.isEmpty() && this.appState.isEmpty();
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
||||
import type { LinearElementEditor } from "@excalidraw/element";
|
||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||
|
||||
import { AppStateDelta, ElementsDelta } from "../src/delta";
|
||||
import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
|
||||
|
||||
describe("ElementsDelta", () => {
|
||||
describe("elements delta calculation", () => {
|
||||
@@ -68,6 +68,251 @@ describe("ElementsDelta", () => {
|
||||
expect(delta.isEmpty()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("squash", () => {
|
||||
it("should not squash when second delta is empty", () => {
|
||||
const updatedDelta = Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1 },
|
||||
{ x: 200, version: 2, versionNonce: 2 },
|
||||
);
|
||||
|
||||
const elementsDelta1 = ElementsDelta.create(
|
||||
{},
|
||||
{},
|
||||
{ id1: updatedDelta },
|
||||
);
|
||||
const elementsDelta2 = ElementsDelta.empty();
|
||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||
|
||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||
expect(elementsDelta).toBe(elementsDelta1);
|
||||
expect(elementsDelta.updated.id1).toBe(updatedDelta);
|
||||
});
|
||||
|
||||
it("should squash mutually exclusive delta types", () => {
|
||||
const addedDelta = Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
||||
);
|
||||
|
||||
const removedDelta = Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||
);
|
||||
|
||||
const updatedDelta = Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1 },
|
||||
{ x: 200, version: 2, versionNonce: 2 },
|
||||
);
|
||||
|
||||
const elementsDelta1 = ElementsDelta.create(
|
||||
{ id1: addedDelta },
|
||||
{ id2: removedDelta },
|
||||
{},
|
||||
);
|
||||
|
||||
const elementsDelta2 = ElementsDelta.create(
|
||||
{},
|
||||
{},
|
||||
{ id3: updatedDelta },
|
||||
);
|
||||
|
||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||
|
||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||
expect(elementsDelta).toBe(elementsDelta1);
|
||||
expect(elementsDelta.added.id1).toBe(addedDelta);
|
||||
expect(elementsDelta.removed.id2).toBe(removedDelta);
|
||||
expect(elementsDelta.updated.id3).toBe(updatedDelta);
|
||||
});
|
||||
|
||||
it("should squash the same delta types", () => {
|
||||
const elementsDelta1 = ElementsDelta.create(
|
||||
{
|
||||
id1: Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
||||
),
|
||||
},
|
||||
{
|
||||
id2: Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||
),
|
||||
},
|
||||
{
|
||||
id3: Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1 },
|
||||
{ x: 200, version: 2, versionNonce: 2 },
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta2 = ElementsDelta.create(
|
||||
{
|
||||
id1: Delta.create(
|
||||
{ y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||
{ y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
||||
),
|
||||
},
|
||||
{
|
||||
id2: Delta.create(
|
||||
{ y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
||||
{ y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
||||
),
|
||||
},
|
||||
{
|
||||
id3: Delta.create(
|
||||
{ y: 100, version: 2, versionNonce: 2 },
|
||||
{ y: 200, version: 3, versionNonce: 3 },
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||
|
||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||
expect(elementsDelta).toBe(elementsDelta1);
|
||||
expect(elementsDelta.added.id1).toEqual(
|
||||
Delta.create(
|
||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
||||
),
|
||||
);
|
||||
expect(elementsDelta.removed.id2).toEqual(
|
||||
Delta.create(
|
||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
||||
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
||||
),
|
||||
);
|
||||
expect(elementsDelta.updated.id3).toEqual(
|
||||
Delta.create(
|
||||
{ x: 100, y: 100, version: 2, versionNonce: 2 },
|
||||
{ x: 200, y: 200, version: 3, versionNonce: 3 },
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should squash different delta types ", () => {
|
||||
// id1: added -> updated => added
|
||||
// id2: removed -> added => added
|
||||
// id3: updated -> removed => removed
|
||||
const elementsDelta1 = ElementsDelta.create(
|
||||
{
|
||||
id1: Delta.create(
|
||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||
{ x: 101, version: 2, versionNonce: 2, isDeleted: false },
|
||||
),
|
||||
},
|
||||
{
|
||||
id2: Delta.create(
|
||||
{ x: 200, version: 1, versionNonce: 1, isDeleted: false },
|
||||
{ x: 201, version: 2, versionNonce: 2, isDeleted: true },
|
||||
),
|
||||
},
|
||||
{
|
||||
id3: Delta.create(
|
||||
{ x: 300, version: 1, versionNonce: 1 },
|
||||
{ x: 301, version: 2, versionNonce: 2 },
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta2 = ElementsDelta.create(
|
||||
{
|
||||
id2: Delta.create(
|
||||
{ y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||
{ y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
||||
),
|
||||
},
|
||||
{
|
||||
id3: Delta.create(
|
||||
{ y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
||||
{ y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
||||
),
|
||||
},
|
||||
{
|
||||
id1: Delta.create(
|
||||
{ y: 100, version: 2, versionNonce: 2 },
|
||||
{ y: 101, version: 3, versionNonce: 3 },
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||
|
||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||
expect(elementsDelta).toBe(elementsDelta1);
|
||||
expect(elementsDelta.added).toEqual({
|
||||
id1: Delta.create(
|
||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||
{ x: 101, y: 101, version: 3, versionNonce: 3, isDeleted: false },
|
||||
),
|
||||
id2: Delta.create(
|
||||
{ x: 200, y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||
{ x: 201, y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
||||
),
|
||||
});
|
||||
expect(elementsDelta.removed).toEqual({
|
||||
id3: Delta.create(
|
||||
{ x: 300, y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
||||
{ x: 301, y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
||||
),
|
||||
});
|
||||
expect(elementsDelta.updated).toEqual({});
|
||||
});
|
||||
|
||||
it("should squash bound elements", () => {
|
||||
const elementsDelta1 = ElementsDelta.create(
|
||||
{},
|
||||
{},
|
||||
{
|
||||
id1: Delta.create(
|
||||
{
|
||||
version: 1,
|
||||
versionNonce: 1,
|
||||
boundElements: [{ id: "t1", type: "text" }],
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
versionNonce: 2,
|
||||
boundElements: [{ id: "t2", type: "text" }],
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta2 = ElementsDelta.create(
|
||||
{},
|
||||
{},
|
||||
{
|
||||
id1: Delta.create(
|
||||
{
|
||||
version: 2,
|
||||
versionNonce: 2,
|
||||
boundElements: [{ id: "a1", type: "arrow" }],
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
versionNonce: 3,
|
||||
boundElements: [{ id: "a2", type: "arrow" }],
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||
|
||||
expect(elementsDelta.updated.id1.deleted.boundElements).toEqual([
|
||||
{ id: "t1", type: "text" },
|
||||
{ id: "a1", type: "arrow" },
|
||||
]);
|
||||
expect(elementsDelta.updated.id1.inserted.boundElements).toEqual([
|
||||
{ id: "t2", type: "text" },
|
||||
{ id: "a2", type: "arrow" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppStateDelta", () => {
|
||||
@@ -215,4 +460,97 @@ describe("AppStateDelta", () => {
|
||||
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||
});
|
||||
});
|
||||
|
||||
describe("squash", () => {
|
||||
it("should not squash when second delta is empty", () => {
|
||||
const delta = Delta.create(
|
||||
{ name: "untitled scene" },
|
||||
{ name: "titled scene" },
|
||||
);
|
||||
|
||||
const appStateDelta1 = AppStateDelta.create(delta);
|
||||
const appStateDelta2 = AppStateDelta.empty();
|
||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||
|
||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||
expect(appStateDelta).toBe(appStateDelta1);
|
||||
expect(appStateDelta.delta).toBe(delta);
|
||||
});
|
||||
|
||||
it("should squash exclusive properties", () => {
|
||||
const delta1 = Delta.create(
|
||||
{ name: "untitled scene" },
|
||||
{ name: "titled scene" },
|
||||
);
|
||||
const delta2 = Delta.create(
|
||||
{ viewBackgroundColor: "#ffffff" },
|
||||
{ viewBackgroundColor: "#000000" },
|
||||
);
|
||||
|
||||
const appStateDelta1 = AppStateDelta.create(delta1);
|
||||
const appStateDelta2 = AppStateDelta.create(delta2);
|
||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||
|
||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||
expect(appStateDelta).toBe(appStateDelta1);
|
||||
expect(appStateDelta.delta).toEqual(
|
||||
Delta.create(
|
||||
{ name: "untitled scene", viewBackgroundColor: "#ffffff" },
|
||||
{ name: "titled scene", viewBackgroundColor: "#000000" },
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should squash selectedElementIds, selectedGroupIds and lockedMultiSelections", () => {
|
||||
const delta1 = Delta.create<Partial<ObservedAppState>>(
|
||||
{
|
||||
name: "untitled scene",
|
||||
selectedElementIds: { id1: true },
|
||||
selectedGroupIds: {},
|
||||
lockedMultiSelections: { g1: true },
|
||||
},
|
||||
{
|
||||
name: "titled scene",
|
||||
selectedElementIds: { id2: true },
|
||||
selectedGroupIds: { g1: true },
|
||||
lockedMultiSelections: {},
|
||||
},
|
||||
);
|
||||
const delta2 = Delta.create<Partial<ObservedAppState>>(
|
||||
{
|
||||
selectedElementIds: { id3: true },
|
||||
selectedGroupIds: { g1: true },
|
||||
lockedMultiSelections: {},
|
||||
},
|
||||
{
|
||||
selectedElementIds: { id2: true },
|
||||
selectedGroupIds: { g2: true, g3: true },
|
||||
lockedMultiSelections: { g3: true },
|
||||
},
|
||||
);
|
||||
|
||||
const appStateDelta1 = AppStateDelta.create(delta1);
|
||||
const appStateDelta2 = AppStateDelta.create(delta2);
|
||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||
|
||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||
expect(appStateDelta).toBe(appStateDelta1);
|
||||
expect(appStateDelta.delta).toEqual(
|
||||
Delta.create<Partial<ObservedAppState>>(
|
||||
{
|
||||
name: "untitled scene",
|
||||
selectedElementIds: { id1: true, id3: true },
|
||||
selectedGroupIds: { g1: true },
|
||||
lockedMultiSelections: { g1: true },
|
||||
},
|
||||
{
|
||||
name: "titled scene",
|
||||
selectedElementIds: { id2: true },
|
||||
selectedGroupIds: { g1: true, g2: true, g3: true },
|
||||
lockedMultiSelections: { g3: true },
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -233,6 +233,8 @@ import {
|
||||
hitElementBoundingBox,
|
||||
isLineElement,
|
||||
isSimpleArrow,
|
||||
StoreDelta,
|
||||
type ApplyToOptions,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -259,6 +261,7 @@ import type {
|
||||
MagicGenerationData,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
SceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
@@ -697,6 +700,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (excalidrawAPI) {
|
||||
const api: ExcalidrawImperativeAPI = {
|
||||
updateScene: this.updateScene,
|
||||
applyDeltas: this.applyDeltas,
|
||||
mutateElement: this.mutateElement,
|
||||
updateLibrary: this.library.updateLibrary,
|
||||
addFiles: this.addFiles,
|
||||
@@ -3938,6 +3942,27 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
public applyDeltas = (
|
||||
deltas: StoreDelta[],
|
||||
options?: ApplyToOptions,
|
||||
): [SceneElementsMap, AppState, boolean] => {
|
||||
// squash all deltas together, starting with a fresh new delta instance
|
||||
const aggregatedDelta = StoreDelta.squash(...deltas);
|
||||
|
||||
// create new instance of elements map & appState, so we don't accidentaly mutate existing ones
|
||||
const nextAppState = { ...this.state };
|
||||
const nextElements = new Map(
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
) as SceneElementsMap;
|
||||
|
||||
return StoreDelta.applyTo(
|
||||
aggregatedDelta,
|
||||
nextElements,
|
||||
nextAppState,
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
|
@@ -175,7 +175,7 @@ export class History {
|
||||
let nextAppState = appState;
|
||||
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 (historyDelta) {
|
||||
try {
|
||||
[nextElements, nextAppState, containsVisibleChange] =
|
||||
|
@@ -137,7 +137,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 18,
|
||||
"version": 13,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -258,7 +258,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 14,
|
||||
"version": 10,
|
||||
"width": 50,
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
@@ -305,11 +305,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 12,
|
||||
"version": 9,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"version": 11,
|
||||
"version": 8,
|
||||
},
|
||||
},
|
||||
"id4": {
|
||||
@@ -384,7 +384,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
"version": 18,
|
||||
"version": 13,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
@@ -393,7 +393,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 17,
|
||||
"version": 12,
|
||||
},
|
||||
},
|
||||
"id4": {
|
||||
@@ -735,7 +735,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 19,
|
||||
"version": 14,
|
||||
"width": 100,
|
||||
"x": 150,
|
||||
"y": -50,
|
||||
@@ -884,7 +884,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
"version": 19,
|
||||
"version": 14,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
@@ -893,7 +893,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 18,
|
||||
"version": 13,
|
||||
},
|
||||
},
|
||||
"id4": {
|
||||
@@ -1242,7 +1242,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 10,
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
@@ -1421,12 +1421,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"version": 11,
|
||||
"version": 10,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": null,
|
||||
"startBinding": null,
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1639,7 +1639,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"version": 8,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -1674,7 +1674,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -1772,11 +1772,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 12,
|
||||
"version": 8,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"version": 9,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
"id1": {
|
||||
@@ -1787,11 +1787,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2202,7 +2202,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -2237,7 +2237,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"width": 100,
|
||||
"x": 500,
|
||||
"y": -500,
|
||||
@@ -2473,7 +2473,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -2488,7 +2488,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -2720,7 +2720,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"version": 7,
|
||||
"verticalAlign": "top",
|
||||
"width": 30,
|
||||
"x": 15,
|
||||
@@ -2780,11 +2780,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"id5": {
|
||||
"deleted": {
|
||||
"containerId": null,
|
||||
"version": 10,
|
||||
"version": 7,
|
||||
},
|
||||
"inserted": {
|
||||
"containerId": "id0",
|
||||
"version": 9,
|
||||
"version": 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2937,7 +2937,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 9,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -2975,7 +2975,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
@@ -3014,7 +3014,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
"verticalAlign": "top",
|
||||
"width": 30,
|
||||
"x": 15,
|
||||
@@ -3041,7 +3041,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"deleted": {
|
||||
"containerId": "id0",
|
||||
"isDeleted": true,
|
||||
"version": 9,
|
||||
"version": 8,
|
||||
},
|
||||
"inserted": {
|
||||
"angle": 0,
|
||||
@@ -3071,7 +3071,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"text": "que pasa",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
@@ -3084,7 +3084,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
"version": 11,
|
||||
"version": 9,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
@@ -3093,7 +3093,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"version": 10,
|
||||
"version": 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3246,7 +3246,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"version": 9,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -3356,7 +3356,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"version": 10,
|
||||
"version": 9,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
@@ -3365,7 +3365,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"version": 9,
|
||||
"version": 8,
|
||||
},
|
||||
},
|
||||
"id1": {
|
||||
@@ -4093,7 +4093,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 80,
|
||||
"x": 15,
|
||||
@@ -4155,11 +4155,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"id1": {
|
||||
"deleted": {
|
||||
"containerId": "id0",
|
||||
"version": 12,
|
||||
"version": 8,
|
||||
},
|
||||
"inserted": {
|
||||
"containerId": null,
|
||||
"version": 9,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -4310,7 +4310,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -4424,11 +4424,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -4617,7 +4617,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 80,
|
||||
"x": 15,
|
||||
@@ -5028,7 +5028,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -5113,7 +5113,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -5126,7 +5126,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
},
|
||||
],
|
||||
"isDeleted": true,
|
||||
"version": 7,
|
||||
"version": 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -5316,7 +5316,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
@@ -5371,7 +5371,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"text": "que pasa",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
@@ -5380,7 +5380,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"inserted": {
|
||||
"containerId": "id0",
|
||||
"isDeleted": true,
|
||||
"version": 7,
|
||||
"version": 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -5527,7 +5527,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"version": 9,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -5784,7 +5784,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"version": 5,
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
@@ -5816,7 +5816,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
@@ -6072,7 +6072,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 20,
|
||||
"y": 0,
|
||||
@@ -6102,7 +6102,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
@@ -6205,11 +6205,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"id3": {
|
||||
"deleted": {
|
||||
"backgroundColor": "#ffc9c9",
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
},
|
||||
"inserted": {
|
||||
"backgroundColor": "transparent",
|
||||
"version": 6,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -6251,12 +6251,12 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"updated": {
|
||||
"id8": {
|
||||
"deleted": {
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
},
|
||||
"inserted": {
|
||||
"version": 6,
|
||||
"version": 7,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
},
|
||||
@@ -7104,7 +7104,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
@@ -7344,7 +7344,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
@@ -7393,11 +7393,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"backgroundColor": "#ffec99",
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
},
|
||||
"inserted": {
|
||||
"backgroundColor": "transparent",
|
||||
"version": 6,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -10326,7 +10326,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
@@ -10378,14 +10378,14 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"version": 7,
|
||||
"version": 8,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
"version": 6,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -15584,7 +15584,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -15622,7 +15622,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"version": 5,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -15658,7 +15658,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -15768,7 +15768,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -15783,7 +15783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -16279,7 +16279,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -16317,7 +16317,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -16353,7 +16353,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -16729,7 +16729,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -16744,7 +16744,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 7,
|
||||
"version": 5,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -16904,7 +16904,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"version": 10,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -16942,7 +16942,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"version": 10,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -16978,7 +16978,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -17119,7 +17119,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"text": "ola",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"version": 9,
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": -200,
|
||||
@@ -17127,7 +17127,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
"version": 8,
|
||||
"version": 7,
|
||||
},
|
||||
},
|
||||
"id2": {
|
||||
@@ -17243,7 +17243,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"containerId": "id0",
|
||||
"height": 25,
|
||||
"textAlign": "center",
|
||||
"version": 10,
|
||||
"version": 9,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -17253,7 +17253,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"containerId": null,
|
||||
"height": 100,
|
||||
"textAlign": "left",
|
||||
"version": 9,
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": -200,
|
||||
@@ -17354,7 +17354,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 12,
|
||||
"version": 10,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -17369,7 +17369,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"version": 9,
|
||||
"version": 7,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
@@ -17527,7 +17527,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -17565,7 +17565,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -17601,7 +17601,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"version": 4,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -17689,7 +17689,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"isDeleted": false,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -17699,7 +17699,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"id1": {
|
||||
"deleted": {
|
||||
"isDeleted": false,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -18239,7 +18239,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"width": 100,
|
||||
"x": -100,
|
||||
"y": -50,
|
||||
@@ -18277,7 +18277,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
"verticalAlign": "middle",
|
||||
"width": 30,
|
||||
"x": -65,
|
||||
@@ -18402,7 +18402,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"isDeleted": false,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -18412,7 +18412,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"id1": {
|
||||
"deleted": {
|
||||
"isDeleted": false,
|
||||
"version": 8,
|
||||
"version": 6,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@@ -801,6 +801,7 @@ export type UnsubscribeCallback = () => void;
|
||||
|
||||
export interface ExcalidrawImperativeAPI {
|
||||
updateScene: InstanceType<typeof App>["updateScene"];
|
||||
applyDeltas: InstanceType<typeof App>["applyDeltas"];
|
||||
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||
resetScene: InstanceType<typeof App>["resetScene"];
|
||||
|
Reference in New Issue
Block a user