|
@@ -55,10 +55,10 @@ import { getNonDeletedGroupIds } from "./groups";
|
|
|
|
|
|
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
|
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
|
|
|
|
|
-import { Scene } from "./Scene";
|
|
|
|
-
|
|
|
|
import { StoreSnapshot } from "./store";
|
|
import { StoreSnapshot } from "./store";
|
|
|
|
|
|
|
|
+import { Scene } from "./Scene";
|
|
|
|
+
|
|
import type { BindableProp, BindingProp } from "./binding";
|
|
import type { BindableProp, BindingProp } from "./binding";
|
|
|
|
|
|
import type { ElementUpdate } from "./mutateElement";
|
|
import type { ElementUpdate } from "./mutateElement";
|
|
@@ -153,10 +153,14 @@ export class Delta<T> {
|
|
/**
|
|
/**
|
|
* Merges two deltas into a new one.
|
|
* 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(
|
|
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 }>(
|
|
public static mergeObjects<T extends { [key: string]: unknown }>(
|
|
prev: T,
|
|
prev: T,
|
|
added: T,
|
|
added: T,
|
|
- removed: T,
|
|
|
|
|
|
+ removed: T = {} as T,
|
|
) {
|
|
) {
|
|
const cloned = { ...prev };
|
|
const cloned = { ...prev };
|
|
|
|
|
|
@@ -520,6 +524,10 @@ export interface DeltaContainer<T> {
|
|
export class AppStateDelta implements DeltaContainer<AppState> {
|
|
export class AppStateDelta implements DeltaContainer<AppState> {
|
|
private constructor(public delta: Delta<ObservedAppState>) {}
|
|
private constructor(public delta: Delta<ObservedAppState>) {}
|
|
|
|
|
|
|
|
+ public static create(delta: Delta<ObservedAppState>): AppStateDelta {
|
|
|
|
+ return new AppStateDelta(delta);
|
|
|
|
+ }
|
|
|
|
+
|
|
public static calculate<T extends ObservedAppState>(
|
|
public static calculate<T extends ObservedAppState>(
|
|
prevAppState: T,
|
|
prevAppState: T,
|
|
nextAppState: T,
|
|
nextAppState: T,
|
|
@@ -550,7 +558,74 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|
}
|
|
}
|
|
|
|
|
|
public squash(delta: AppStateDelta): this {
|
|
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;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -562,11 +637,13 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|
const {
|
|
const {
|
|
selectedElementIds: deletedSelectedElementIds = {},
|
|
selectedElementIds: deletedSelectedElementIds = {},
|
|
selectedGroupIds: deletedSelectedGroupIds = {},
|
|
selectedGroupIds: deletedSelectedGroupIds = {},
|
|
|
|
+ lockedMultiSelections: deletedLockedMultiSelections = {},
|
|
} = this.delta.deleted;
|
|
} = this.delta.deleted;
|
|
|
|
|
|
const {
|
|
const {
|
|
selectedElementIds: insertedSelectedElementIds = {},
|
|
selectedElementIds: insertedSelectedElementIds = {},
|
|
selectedGroupIds: insertedSelectedGroupIds = {},
|
|
selectedGroupIds: insertedSelectedGroupIds = {},
|
|
|
|
+ lockedMultiSelections: insertedLockedMultiSelections = {},
|
|
selectedLinearElement: insertedSelectedLinearElement,
|
|
selectedLinearElement: insertedSelectedLinearElement,
|
|
...directlyApplicablePartial
|
|
...directlyApplicablePartial
|
|
} = this.delta.inserted;
|
|
} = this.delta.inserted;
|
|
@@ -583,6 +660,12 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|
deletedSelectedGroupIds,
|
|
deletedSelectedGroupIds,
|
|
);
|
|
);
|
|
|
|
|
|
|
|
+ const mergedLockedMultiSelections = Delta.mergeObjects(
|
|
|
|
+ appState.lockedMultiSelections,
|
|
|
|
+ insertedLockedMultiSelections,
|
|
|
|
+ deletedLockedMultiSelections,
|
|
|
|
+ );
|
|
|
|
+
|
|
const selectedLinearElement =
|
|
const selectedLinearElement =
|
|
insertedSelectedLinearElement &&
|
|
insertedSelectedLinearElement &&
|
|
nextElements.has(insertedSelectedLinearElement.elementId)
|
|
nextElements.has(insertedSelectedLinearElement.elementId)
|
|
@@ -600,6 +683,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|
...directlyApplicablePartial,
|
|
...directlyApplicablePartial,
|
|
selectedElementIds: mergedSelectedElementIds,
|
|
selectedElementIds: mergedSelectedElementIds,
|
|
selectedGroupIds: mergedSelectedGroupIds,
|
|
selectedGroupIds: mergedSelectedGroupIds,
|
|
|
|
+ lockedMultiSelections: mergedLockedMultiSelections,
|
|
selectedLinearElement:
|
|
selectedLinearElement:
|
|
typeof insertedSelectedLinearElement !== "undefined"
|
|
typeof insertedSelectedLinearElement !== "undefined"
|
|
? selectedLinearElement
|
|
? selectedLinearElement
|
|
@@ -904,12 +988,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|
"lockedMultiSelections",
|
|
"lockedMultiSelections",
|
|
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
|
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
|
);
|
|
);
|
|
- Delta.diffObjects(
|
|
|
|
- deleted,
|
|
|
|
- inserted,
|
|
|
|
- "activeLockedId",
|
|
|
|
- (prevValue) => (prevValue ?? null) as ValueOf<T["activeLockedId"]>,
|
|
|
|
- );
|
|
|
|
} 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 appstate change deltas.`);
|
|
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">;
|
|
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
|
|
|
|
|
export type ApplyToOptions = {
|
|
export type ApplyToOptions = {
|
|
- excludedProperties: Set<keyof ElementPartial>;
|
|
|
|
|
|
+ excludedProperties?: Set<keyof ElementPartial>;
|
|
};
|
|
};
|
|
|
|
|
|
type ApplyToFlags = {
|
|
type ApplyToFlags = {
|
|
containsVisibleDifference: boolean;
|
|
containsVisibleDifference: boolean;
|
|
containsZindexDifference: boolean;
|
|
containsZindexDifference: boolean;
|
|
|
|
+ applyDirection: "forward" | "backward" | undefined;
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1044,6 +1123,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
deleted.version !== inserted.version
|
|
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(
|
|
private static validate(
|
|
elementsDelta: ElementsDelta,
|
|
elementsDelta: ElementsDelta,
|
|
type: "added" | "removed" | "updated",
|
|
type: "added" | "removed" | "updated",
|
|
@@ -1052,6 +1140,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
|
if (
|
|
if (
|
|
!this.satisfiesCommmonInvariants(delta) ||
|
|
!this.satisfiesCommmonInvariants(delta) ||
|
|
|
|
+ !this.satisfiesUniqueInvariants(elementsDelta, id) ||
|
|
!satifiesSpecialInvariants(delta)
|
|
!satifiesSpecialInvariants(delta)
|
|
) {
|
|
) {
|
|
console.error(
|
|
console.error(
|
|
@@ -1311,9 +1400,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
public applyTo(
|
|
public applyTo(
|
|
elements: SceneElementsMap,
|
|
elements: SceneElementsMap,
|
|
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
|
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
|
- options: ApplyToOptions = {
|
|
|
|
- excludedProperties: new Set(),
|
|
|
|
- },
|
|
|
|
|
|
+ options?: ApplyToOptions,
|
|
): [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>;
|
|
@@ -1321,22 +1408,28 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
const flags: ApplyToFlags = {
|
|
const flags: ApplyToFlags = {
|
|
containsVisibleDifference: false,
|
|
containsVisibleDifference: false,
|
|
containsZindexDifference: false,
|
|
containsZindexDifference: false,
|
|
|
|
+ applyDirection: undefined,
|
|
};
|
|
};
|
|
|
|
|
|
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
|
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
|
try {
|
|
try {
|
|
const applyDeltas = ElementsDelta.createApplier(
|
|
const applyDeltas = ElementsDelta.createApplier(
|
|
|
|
+ elements,
|
|
nextElements,
|
|
nextElements,
|
|
snapshot,
|
|
snapshot,
|
|
- options,
|
|
|
|
flags,
|
|
flags,
|
|
|
|
+ options,
|
|
);
|
|
);
|
|
|
|
|
|
const addedElements = applyDeltas(this.added);
|
|
const addedElements = applyDeltas(this.added);
|
|
const removedElements = applyDeltas(this.removed);
|
|
const removedElements = applyDeltas(this.removed);
|
|
const updatedElements = applyDeltas(this.updated);
|
|
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
|
|
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
|
changedElements = new Map([
|
|
changedElements = new Map([
|
|
@@ -1360,22 +1453,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
}
|
|
}
|
|
|
|
|
|
try {
|
|
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 = ElementsDelta.reorderElements(
|
|
nextElements,
|
|
nextElements,
|
|
changedElements,
|
|
changedElements,
|
|
flags,
|
|
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) {
|
|
} catch (e) {
|
|
console.error(
|
|
console.error(
|
|
`Couldn't mutate elements after applying elements change`,
|
|
`Couldn't mutate elements after applying elements change`,
|
|
@@ -1391,47 +1477,112 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
}
|
|
}
|
|
|
|
|
|
public squash(delta: ElementsDelta): this {
|
|
public squash(delta: ElementsDelta): this {
|
|
|
|
+ if (delta.isEmpty()) {
|
|
|
|
+ return this;
|
|
|
|
+ }
|
|
|
|
+
|
|
const { added, removed, updated } = delta;
|
|
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)) {
|
|
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) {
|
|
if (!prevDelta) {
|
|
this.added[id] = nextDelta;
|
|
this.added[id] = nextDelta;
|
|
} else {
|
|
} 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)) {
|
|
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) {
|
|
if (!prevDelta) {
|
|
this.removed[id] = nextDelta;
|
|
this.removed[id] = nextDelta;
|
|
} else {
|
|
} 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)) {
|
|
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) {
|
|
if (!prevDelta) {
|
|
this.updated[id] = nextDelta;
|
|
this.updated[id] = nextDelta;
|
|
} else {
|
|
} 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;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
|
|
private static createApplier =
|
|
private static createApplier =
|
|
(
|
|
(
|
|
|
|
+ prevElements: SceneElementsMap,
|
|
nextElements: SceneElementsMap,
|
|
nextElements: SceneElementsMap,
|
|
snapshot: StoreSnapshot["elements"],
|
|
snapshot: StoreSnapshot["elements"],
|
|
- options: ApplyToOptions,
|
|
|
|
flags: ApplyToFlags,
|
|
flags: ApplyToFlags,
|
|
|
|
+ options?: ApplyToOptions,
|
|
) =>
|
|
) =>
|
|
(deltas: Record<string, Delta<ElementPartial>>) => {
|
|
(deltas: Record<string, Delta<ElementPartial>>) => {
|
|
const getElement = ElementsDelta.createGetter(
|
|
const getElement = ElementsDelta.createGetter(
|
|
@@ -1444,15 +1595,26 @@ 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(
|
|
|
|
|
|
+ const nextElement = ElementsDelta.applyDelta(
|
|
element,
|
|
element,
|
|
delta,
|
|
delta,
|
|
- options,
|
|
|
|
flags,
|
|
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;
|
|
return acc;
|
|
@@ -1497,8 +1659,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
private static applyDelta(
|
|
private static applyDelta(
|
|
element: OrderedExcalidrawElement,
|
|
element: OrderedExcalidrawElement,
|
|
delta: Delta<ElementPartial>,
|
|
delta: Delta<ElementPartial>,
|
|
- options: ApplyToOptions,
|
|
|
|
flags: ApplyToFlags,
|
|
flags: ApplyToFlags,
|
|
|
|
+ options?: ApplyToOptions,
|
|
) {
|
|
) {
|
|
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
|
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
|
|
|
|
|
@@ -1512,7 +1674,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
- if (options.excludedProperties.has(key)) {
|
|
|
|
|
|
+ if (options?.excludedProperties?.has(key)) {
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -1552,7 +1714,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
delta.deleted.index !== delta.inserted.index;
|
|
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(
|
|
private resolveConflicts(
|
|
prevElements: SceneElementsMap,
|
|
prevElements: SceneElementsMap,
|
|
nextElements: SceneElementsMap,
|
|
nextElements: SceneElementsMap,
|
|
|
|
+ applyDirection: "forward" | "backward" = "forward",
|
|
) {
|
|
) {
|
|
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
|
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
|
const updater = (
|
|
const updater = (
|
|
@@ -1603,21 +1766,36 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
return;
|
|
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;
|
|
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
|
|
// 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
|
|
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
|
affectedElement = newElementWith(
|
|
affectedElement = newElementWith(
|
|
nextElement,
|
|
nextElement,
|
|
- updates as ElementUpdate<OrderedExcalidrawElement>,
|
|
|
|
|
|
+ {
|
|
|
|
+ ...elementUpdates,
|
|
|
|
+ version: nextVersion,
|
|
|
|
+ },
|
|
|
|
+ true,
|
|
);
|
|
);
|
|
} else {
|
|
} 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);
|
|
nextAffectedElements.set(affectedElement.id, affectedElement);
|
|
@@ -1722,6 +1900,31 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
|
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(
|
|
private static redrawTextBoundingBoxes(
|
|
scene: Scene,
|
|
scene: Scene,
|
|
changed: Map<string, OrderedExcalidrawElement>,
|
|
changed: Map<string, OrderedExcalidrawElement>,
|
|
@@ -1776,6 +1979,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|
) {
|
|
) {
|
|
for (const element of changed.values()) {
|
|
for (const element of changed.values()) {
|
|
if (!element.isDeleted && isBindableElement(element)) {
|
|
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, {
|
|
updateBoundElements(element, scene, {
|
|
changedElements: changed,
|
|
changedElements: changed,
|
|
});
|
|
});
|