Recalculate the history entry
This commit is contained in:
parent
bc1a71e772
commit
9093908663
@ -1098,73 +1098,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delta/s based on the existing elements.
|
||||
*
|
||||
* @param elements current elements
|
||||
* @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated
|
||||
* @returns new instance with modified delta/s
|
||||
*/
|
||||
public applyLatestChanges(
|
||||
elements: SceneElementsMap,
|
||||
modifierOptions: "deleted" | "inserted",
|
||||
): ElementsDelta {
|
||||
const modifier =
|
||||
(element: OrderedExcalidrawElement) => (partial: ElementPartial) => {
|
||||
const latestPartial: { [key: string]: unknown } = {};
|
||||
|
||||
for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
|
||||
// do not update following props:
|
||||
// - `version` and `versionNonce`, as they should keep it's original value
|
||||
// - `boundElements`, as it is a reference value which is postprocessed to contain only deleted/inserted keys
|
||||
switch (key) {
|
||||
case "version":
|
||||
case "versionNonce":
|
||||
case "boundElements":
|
||||
latestPartial[key] = partial[key];
|
||||
break;
|
||||
default:
|
||||
latestPartial[key] = element[key];
|
||||
}
|
||||
}
|
||||
|
||||
return latestPartial;
|
||||
};
|
||||
|
||||
const applyLatestChangesInternal = (
|
||||
deltas: Record<string, Delta<ElementPartial>>,
|
||||
) => {
|
||||
const modifiedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||
|
||||
for (const [id, delta] of Object.entries(deltas)) {
|
||||
const existingElement = elements.get(id);
|
||||
|
||||
if (existingElement) {
|
||||
const modifiedDelta = Delta.create(
|
||||
delta.deleted,
|
||||
delta.inserted,
|
||||
modifier(existingElement),
|
||||
modifierOptions,
|
||||
);
|
||||
|
||||
modifiedDeltas[id] = modifiedDelta;
|
||||
} else {
|
||||
modifiedDeltas[id] = delta;
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedDeltas;
|
||||
};
|
||||
|
||||
const added = applyLatestChangesInternal(this.added);
|
||||
const removed = applyLatestChangesInternal(this.removed);
|
||||
const updated = applyLatestChangesInternal(this.updated);
|
||||
|
||||
return ElementsDelta.create(added, removed, updated, {
|
||||
shouldRedistribute: true, // redistribute the deltas as `isDeleted` could have been updated
|
||||
});
|
||||
}
|
||||
|
||||
public applyTo(
|
||||
elements: SceneElementsMap,
|
||||
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
||||
|
@ -213,16 +213,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
|
||||
storeDelta = delta;
|
||||
} else {
|
||||
// calculate the deltas based on the previous and next 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);
|
||||
storeDelta = StoreDelta.calculate(prevSnapshot, snapshot);
|
||||
}
|
||||
|
||||
if (!storeDelta.isEmpty()) {
|
||||
@ -505,6 +496,24 @@ export class StoreDelta {
|
||||
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.
|
||||
*/
|
||||
@ -538,23 +547,6 @@ export class StoreDelta {
|
||||
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",
|
||||
) {
|
||||
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.
|
||||
*/
|
||||
|
@ -13,7 +13,7 @@ import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||
|
||||
import type { AppState } from "./types";
|
||||
|
||||
class HistoryEntry extends StoreDelta {
|
||||
class HistoryDelta extends StoreDelta {
|
||||
/**
|
||||
* Apply the delta to the passed elements and appState, does not modify the snapshot.
|
||||
*/
|
||||
@ -47,23 +47,18 @@ class HistoryEntry extends StoreDelta {
|
||||
/**
|
||||
* Overriding once to avoid type casting everywhere.
|
||||
*/
|
||||
public static override inverse(delta: StoreDelta): HistoryEntry {
|
||||
return super.inverse(delta) as HistoryEntry;
|
||||
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 applyLatestChanges(
|
||||
delta: StoreDelta,
|
||||
elements: SceneElementsMap,
|
||||
modifierOptions: "deleted" | "inserted",
|
||||
): HistoryEntry {
|
||||
return super.applyLatestChanges(
|
||||
delta,
|
||||
elements,
|
||||
modifierOptions,
|
||||
) as HistoryEntry;
|
||||
public static override inverse(delta: StoreDelta): HistoryDelta {
|
||||
return super.inverse(delta) as HistoryDelta;
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,8 +74,8 @@ export class History {
|
||||
[HistoryChangedEvent]
|
||||
>();
|
||||
|
||||
public readonly undoStack: HistoryEntry[] = [];
|
||||
public readonly redoStack: HistoryEntry[] = [];
|
||||
public readonly undoStack: HistoryDelta[] = [];
|
||||
public readonly redoStack: HistoryDelta[] = [];
|
||||
|
||||
public get isUndoStackEmpty() {
|
||||
return this.undoStack.length === 0;
|
||||
@ -102,16 +97,16 @@ export class History {
|
||||
* Do not re-record history entries, which were already pushed to undo / redo stack, as part of history action.
|
||||
*/
|
||||
public record(delta: StoreDelta) {
|
||||
if (delta.isEmpty() || delta instanceof HistoryEntry) {
|
||||
if (delta.isEmpty() || delta instanceof HistoryDelta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
// as a simple click (unselect) could lead to losing all the redo entries
|
||||
// only reset on non empty elements changes!
|
||||
@ -128,7 +123,7 @@ export class History {
|
||||
elements,
|
||||
appState,
|
||||
() => History.pop(this.undoStack),
|
||||
(entry: HistoryEntry) => History.push(this.redoStack, entry, elements),
|
||||
(entry: HistoryDelta) => History.push(this.redoStack, entry),
|
||||
);
|
||||
}
|
||||
|
||||
@ -137,20 +132,20 @@ export class History {
|
||||
elements,
|
||||
appState,
|
||||
() => History.pop(this.redoStack),
|
||||
(entry: HistoryEntry) => History.push(this.undoStack, entry, elements),
|
||||
(entry: HistoryDelta) => History.push(this.undoStack, entry),
|
||||
);
|
||||
}
|
||||
|
||||
private perform(
|
||||
elements: SceneElementsMap,
|
||||
appState: AppState,
|
||||
pop: () => HistoryEntry | null,
|
||||
push: (entry: HistoryEntry) => void,
|
||||
pop: () => HistoryDelta | null,
|
||||
push: (entry: HistoryDelta) => void,
|
||||
): [SceneElementsMap, AppState] | void {
|
||||
try {
|
||||
let historyEntry = pop();
|
||||
let historyDelta = pop();
|
||||
|
||||
if (historyEntry === null) {
|
||||
if (historyDelta === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -163,10 +158,10 @@ export class History {
|
||||
let containsVisibleChange = false;
|
||||
|
||||
// iterate through the history entries in case they result in no visible changes
|
||||
while (historyEntry) {
|
||||
while (historyDelta) {
|
||||
try {
|
||||
[nextElements, nextAppState, containsVisibleChange] =
|
||||
historyEntry.applyTo(nextElements, nextAppState, prevSnapshot);
|
||||
historyDelta.applyTo(nextElements, nextAppState, prevSnapshot);
|
||||
|
||||
const nextSnapshot = prevSnapshot.maybeClone(
|
||||
action,
|
||||
@ -174,24 +169,31 @@ export class History {
|
||||
nextAppState,
|
||||
);
|
||||
|
||||
const change = StoreChange.create(prevSnapshot, nextSnapshot);
|
||||
|
||||
// update the history entry, so that it's the a new history entry instance
|
||||
// and so that it contains the latest changes in both inserted and deleted partials
|
||||
// including version and versionNonce or properties which were in the meantime updated by a remote client
|
||||
historyDelta = HistoryDelta.calculate(prevSnapshot, nextSnapshot);
|
||||
|
||||
// schedule immediate capture, so that it's emitted for the sync purposes
|
||||
this.store.scheduleMicroAction({
|
||||
action,
|
||||
change: StoreChange.create(prevSnapshot, nextSnapshot),
|
||||
delta: historyEntry,
|
||||
change,
|
||||
delta: historyDelta,
|
||||
});
|
||||
|
||||
prevSnapshot = nextSnapshot;
|
||||
} finally {
|
||||
// make sure to always push, even if the delta is corrupted
|
||||
push(historyEntry);
|
||||
push(historyDelta);
|
||||
}
|
||||
|
||||
if (containsVisibleChange) {
|
||||
break;
|
||||
}
|
||||
|
||||
historyEntry = pop();
|
||||
historyDelta = pop();
|
||||
}
|
||||
|
||||
return [nextElements, nextAppState];
|
||||
@ -204,7 +206,7 @@ export class History {
|
||||
}
|
||||
}
|
||||
|
||||
private static pop(stack: HistoryEntry[]): HistoryEntry | null {
|
||||
private static pop(stack: HistoryDelta[]): HistoryDelta | null {
|
||||
if (!stack.length) {
|
||||
return null;
|
||||
}
|
||||
@ -218,18 +220,8 @@ export class History {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static push(
|
||||
stack: HistoryEntry[],
|
||||
entry: HistoryEntry,
|
||||
prevElements: SceneElementsMap,
|
||||
) {
|
||||
const inversedEntry = HistoryEntry.inverse(entry);
|
||||
const updatedEntry = HistoryEntry.applyLatestChanges(
|
||||
inversedEntry,
|
||||
prevElements,
|
||||
"inserted",
|
||||
);
|
||||
|
||||
return stack.push(updatedEntry);
|
||||
private static push(stack: HistoryDelta[], entry: HistoryDelta) {
|
||||
const inversedEntry = HistoryDelta.inverse(entry);
|
||||
return stack.push(inversedEntry);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user