feat: various delta improvements (#9571)

This commit is contained in:
Marcel Mraz
2025-06-09 09:55:35 +02:00
committed by GitHub
parent d4e85a9480
commit d108053351
16 changed files with 1423 additions and 687 deletions
+13 -15
View File
@@ -103,6 +103,7 @@ import {
} from "@excalidraw/common";
import {
getObservedAppState,
getCommonBounds,
getElementAbsoluteCoords,
bindOrUnbindLinearElements,
@@ -260,7 +261,6 @@ import type {
ExcalidrawNonSelectionElement,
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
SceneElementsMap,
} from "@excalidraw/element/types";
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
@@ -702,6 +702,8 @@ class App extends React.Component<AppProps, AppState> {
addFiles: this.addFiles,
resetScene: this.resetScene,
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
getSceneElementsMapIncludingDeleted:
this.getSceneElementsMapIncludingDeleted,
history: {
clear: this.resetHistory,
},
@@ -3909,22 +3911,18 @@ class App extends React.Component<AppProps, AppState> {
}) => {
const { elements, appState, collaborators, captureUpdate } = sceneData;
const nextElements = elements ? syncInvalidIndices(elements) : undefined;
if (captureUpdate) {
const nextElementsMap = elements
? (arrayToMap(nextElements ?? []) as SceneElementsMap)
: undefined;
const nextAppState = appState
? // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState`
Object.assign({}, this.store.snapshot.appState, appState)
const observedAppState = appState
? getObservedAppState({
...this.store.snapshot.appState,
...appState,
})
: undefined;
this.store.scheduleMicroAction({
action: captureUpdate,
elements: nextElementsMap,
appState: nextAppState,
elements: elements ?? [],
appState: observedAppState,
});
}
@@ -3932,8 +3930,8 @@ class App extends React.Component<AppProps, AppState> {
this.setState(appState);
}
if (nextElements) {
this.scene.replaceAllElements(nextElements);
if (elements) {
this.scene.replaceAllElements(elements);
}
if (collaborators) {
@@ -10550,7 +10548,7 @@ class App extends React.Component<AppProps, AppState> {
// otherwise we would end up with duplicated fractional indices on undo
this.store.scheduleMicroAction({
action: CaptureUpdateAction.NEVER,
elements: arrayToMap(elements) as SceneElementsMap,
elements,
appState: undefined,
});
+108 -45
View File
@@ -4,14 +4,81 @@ import {
CaptureUpdateAction,
StoreChange,
StoreDelta,
type Store,
} from "@excalidraw/element";
import type { StoreSnapshot, Store } from "@excalidraw/element";
import type { SceneElementsMap } from "@excalidraw/element/types";
import type { AppState } from "./types";
class HistoryEntry extends StoreDelta {}
export class HistoryDelta extends StoreDelta {
/**
* Apply the delta to the passed elements and appState, does not modify the snapshot.
*/
public applyTo(
elements: SceneElementsMap,
appState: AppState,
snapshot: StoreSnapshot,
): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = this.elements.applyTo(
elements,
// used to fallback into local snapshot in case we couldn't apply the delta
// due to a missing (force deleted) elements in the scene
snapshot.elements,
// we don't want to apply the `version` and `versionNonce` properties for history
// as we always need to end up with a new version due to collaboration,
// approaching each undo / redo as a new user action
{
excludedProperties: new Set(["version", "versionNonce"]),
},
);
const [nextAppState, appStateContainsVisibleChange] = this.appState.applyTo(
appState,
nextElements,
);
const appliedVisibleChanges =
elementsContainVisibleChange || appStateContainsVisibleChange;
return [nextElements, nextAppState, appliedVisibleChanges];
}
/**
* Overriding once to avoid type casting everywhere.
*/
public static override calculate(
prevSnapshot: StoreSnapshot,
nextSnapshot: StoreSnapshot,
) {
return super.calculate(prevSnapshot, nextSnapshot) as HistoryDelta;
}
/**
* Overriding once to avoid type casting everywhere.
*/
public static override inverse(delta: StoreDelta): HistoryDelta {
return super.inverse(delta) as HistoryDelta;
}
/**
* Overriding once to avoid type casting everywhere.
*/
public static override applyLatestChanges(
delta: StoreDelta,
prevElements: SceneElementsMap,
nextElements: SceneElementsMap,
modifierOptions?: "deleted" | "inserted",
) {
return super.applyLatestChanges(
delta,
prevElements,
nextElements,
modifierOptions,
) as HistoryDelta;
}
}
export class HistoryChangedEvent {
constructor(
@@ -25,8 +92,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;
@@ -48,16 +115,16 @@ export class History {
* Do not re-record history entries, which were already pushed to undo / redo stack, as part of history action.
*/
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!
@@ -74,7 +141,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),
);
}
@@ -83,20 +150,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;
}
@@ -108,41 +175,47 @@ export class History {
let nextAppState = appState;
let containsVisibleChange = false;
// iterate through the history entries in case they result in no visible changes
while (historyEntry) {
// iterate through the history entries in case ;they result in no visible changes
while (historyDelta) {
try {
[nextElements, nextAppState, containsVisibleChange] =
StoreDelta.applyTo(
historyEntry,
nextElements,
nextAppState,
prevSnapshot,
);
historyDelta.applyTo(nextElements, nextAppState, prevSnapshot);
const prevElements = prevSnapshot.elements;
const nextSnapshot = prevSnapshot.maybeClone(
action,
nextElements,
nextAppState,
);
// schedule immediate capture, so that it's emitted for the sync purposes
this.store.scheduleMicroAction({
action,
change: StoreChange.create(prevSnapshot, nextSnapshot),
delta: historyEntry,
});
const change = StoreChange.create(prevSnapshot, nextSnapshot);
const delta = HistoryDelta.applyLatestChanges(
historyDelta,
prevElements,
nextElements,
);
if (!delta.isEmpty()) {
// schedule immediate capture, so that it's emitted for the sync purposes
this.store.scheduleMicroAction({
action,
change,
delta,
});
historyDelta = delta;
}
prevSnapshot = nextSnapshot;
} 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];
@@ -155,7 +228,7 @@ export class History {
}
}
private static pop(stack: HistoryEntry[]): HistoryEntry | null {
private static pop(stack: HistoryDelta[]): HistoryDelta | null {
if (!stack.length) {
return null;
}
@@ -169,18 +242,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);
}
}
@@ -1269,12 +1269,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -20,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -1420,14 +1422,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1014066025,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1604849351,
"versionNonce": 1505387817,
"width": 20,
"x": 20,
"y": 30,
@@ -1459,7 +1461,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 23633383,
"versionNonce": 915032327,
"width": 20,
"x": -10,
"y": 0,
@@ -1511,12 +1513,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -1563,12 +1567,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -1598,9 +1604,11 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"id0": {
"deleted": {
"index": "a2",
"version": 4,
},
"inserted": {
"index": "a0",
"version": 3,
},
},
},
@@ -1745,14 +1753,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1014066025,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1604849351,
"versionNonce": 1505387817,
"width": 20,
"x": 20,
"y": 30,
@@ -1784,7 +1792,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 23633383,
"versionNonce": 915032327,
"width": 20,
"x": -10,
"y": 0,
@@ -1836,12 +1844,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -1888,12 +1898,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -1923,9 +1935,11 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"id0": {
"deleted": {
"index": "a2",
"version": 4,
},
"inserted": {
"index": "a0",
"version": 3,
},
},
},
@@ -2131,12 +2145,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -20,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -2287,7 +2303,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1116226695,
"versionNonce": 1014066025,
"width": 10,
"x": -20,
"y": -10,
@@ -2339,12 +2355,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -20,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -2370,9 +2388,11 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"id0": {
"deleted": {
"isDeleted": true,
"version": 4,
},
"inserted": {
"isDeleted": false,
"version": 3,
},
},
},
@@ -2551,14 +2571,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1014066025,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"versionNonce": 400692809,
"versionNonce": 1604849351,
"width": 10,
"x": -10,
"y": 0,
@@ -2610,12 +2630,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -20,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -2662,12 +2684,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 5,
"width": 10,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 4,
},
},
},
@@ -2827,7 +2851,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 493213705,
"versionNonce": 81784553,
"width": 20,
"x": -10,
"y": 0,
@@ -2854,14 +2878,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1014066025,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 915032327,
"versionNonce": 747212839,
"width": 20,
"x": 20,
"y": 30,
@@ -2913,12 +2937,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -2965,12 +2991,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -3020,9 +3048,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"groupIds": [
"id9",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
"id3": {
@@ -3030,9 +3060,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"groupIds": [
"id9",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
},
@@ -3186,7 +3218,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 908564423,
"versionNonce": 1359939303,
"width": 20,
"x": -10,
"y": 0,
@@ -3211,14 +3243,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 60,
"roughness": 2,
"roundness": null,
"seed": 1315507081,
"seed": 640725609,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"versionNonce": 406373543,
"versionNonce": 908564423,
"width": 20,
"x": 20,
"y": 30,
@@ -3270,12 +3302,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -3322,12 +3356,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -3349,9 +3385,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"strokeColor": "#e03131",
"version": 4,
},
"inserted": {
"strokeColor": "#1e1e1e",
"version": 3,
},
},
},
@@ -3372,9 +3410,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"backgroundColor": "#a5d8ff",
"version": 5,
},
"inserted": {
"backgroundColor": "transparent",
"version": 4,
},
},
},
@@ -3395,9 +3435,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"fillStyle": "cross-hatch",
"version": 6,
},
"inserted": {
"fillStyle": "solid",
"version": 5,
},
},
},
@@ -3418,9 +3460,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"strokeStyle": "dotted",
"version": 7,
},
"inserted": {
"strokeStyle": "solid",
"version": 6,
},
},
},
@@ -3441,9 +3485,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"roughness": 2,
"version": 8,
},
"inserted": {
"roughness": 1,
"version": 7,
},
},
},
@@ -3464,9 +3510,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"id3": {
"deleted": {
"opacity": 60,
"version": 9,
},
"inserted": {
"opacity": 100,
"version": 8,
},
},
},
@@ -3500,6 +3548,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 2,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"version": 4,
},
"inserted": {
"backgroundColor": "transparent",
@@ -3508,6 +3557,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"version": 3,
},
},
},
@@ -3652,14 +3702,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1150084233,
"seed": 1116226695,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1604849351,
"versionNonce": 23633383,
"width": 20,
"x": 20,
"y": 30,
@@ -3743,12 +3793,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -3795,12 +3847,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -3822,9 +3876,11 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"id3": {
"deleted": {
"index": "Zz",
"version": 4,
},
"inserted": {
"index": "a1",
"version": 3,
},
},
},
@@ -3969,14 +4025,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1014066025,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 23633383,
"versionNonce": 915032327,
"width": 20,
"x": 20,
"y": 30,
@@ -4060,12 +4116,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -4112,12 +4170,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -4139,9 +4199,11 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"id3": {
"deleted": {
"index": "Zz",
"version": 4,
},
"inserted": {
"index": "a1",
"version": 3,
},
},
},
@@ -4296,7 +4358,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"type": "rectangle",
"updated": 1,
"version": 5,
"versionNonce": 1723083209,
"versionNonce": 1006504105,
"width": 20,
"x": -10,
"y": 0,
@@ -4321,14 +4383,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 238820263,
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"versionNonce": 760410951,
"versionNonce": 289600103,
"width": 20,
"x": 20,
"y": 30,
@@ -4380,12 +4442,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -4432,12 +4496,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 20,
"x": 20,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -4487,9 +4553,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"groupIds": [
"id9",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
"id3": {
@@ -4497,9 +4565,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"groupIds": [
"id9",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
},
@@ -4526,21 +4596,25 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"id0": {
"deleted": {
"groupIds": [],
"version": 5,
},
"inserted": {
"groupIds": [
"id9",
],
"version": 4,
},
},
"id3": {
"deleted": {
"groupIds": [],
"version": 5,
},
"inserted": {
"groupIds": [
"id9",
],
"version": 4,
},
},
},
@@ -5594,14 +5668,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1604849351,
"seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 493213705,
"versionNonce": 915032327,
"width": 10,
"x": 12,
"y": 0,
@@ -5653,12 +5727,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -5705,12 +5781,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": 12,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -6786,7 +6864,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 81784553,
"versionNonce": 1723083209,
"width": 10,
"x": -10,
"y": 0,
@@ -6813,14 +6891,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 238820263,
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 747212839,
"versionNonce": 760410951,
"width": 10,
"x": 12,
"y": 0,
@@ -6872,12 +6950,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -6924,12 +7004,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": 12,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
@@ -7001,9 +7083,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"groupIds": [
"id12",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
"id3": {
@@ -7011,9 +7095,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"groupIds": [
"id12",
],
"version": 4,
},
"inserted": {
"groupIds": [],
"version": 3,
},
},
},
@@ -9822,12 +9908,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 10,
"x": -20,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 2,
},
},
},
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
"type": "rectangle",
"updated": 1,
"version": 5,
"versionNonce": 1505387817,
"versionNonce": 23633383,
"width": 30,
"x": 30,
"y": 20,
@@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1604849351,
"seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 915032327,
"versionNonce": 81784553,
"width": 30,
"x": -10,
"y": 60,
@@ -89,7 +89,7 @@ exports[`move element > rectangle 5`] = `
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1116226695,
"versionNonce": 1014066025,
"width": 30,
"x": 0,
"y": 40,
@@ -126,7 +126,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1723083209,
"versionNonce": 1006504105,
"width": 100,
"x": 0,
"y": 0,
@@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 6`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1150084233,
"seed": 1116226695,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 1051383431,
"versionNonce": 1984422985,
"width": 300,
"x": 201,
"y": 2,
@@ -208,7 +208,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"roundness": {
"type": 2,
},
"seed": 1604849351,
"seed": 23633383,
"startArrowhead": null,
"startBinding": {
"elementId": "id0",
@@ -221,7 +221,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"type": "arrow",
"updated": 1,
"version": 11,
"versionNonce": 1996028265,
"versionNonce": 1573789895,
"width": "81.00000",
"x": "110.00000",
"y": 50,
@@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
"type": "arrow",
"updated": 1,
"version": 8,
"versionNonce": 400692809,
"versionNonce": 1604849351,
"width": 70,
"x": 30,
"y": 30,
@@ -105,7 +105,7 @@ exports[`multi point mode in linear elements > line 3`] = `
"type": "line",
"updated": 1,
"version": 8,
"versionNonce": 400692809,
"versionNonce": 1604849351,
"width": 70,
"x": 30,
"y": 30,
File diff suppressed because it is too large Load Diff
+14 -9
View File
@@ -431,12 +431,17 @@ export const assertElements = <T extends AllPossibleKeys<ExcalidrawElement>>(
expect(h.state.selectedElementIds).toEqual(selectedElementIds);
};
const stripSeed = (deltas: Record<string, { deleted: any; inserted: any }>) =>
const stripProps = (
deltas: Record<string, { deleted: any; inserted: any }>,
props: string[],
) =>
Object.entries(deltas).reduce((acc, curr) => {
const { inserted, deleted, ...rest } = curr[1];
delete inserted.seed;
delete deleted.seed;
for (const prop of props) {
delete inserted[prop];
delete deleted[prop];
}
acc[curr[0]] = {
inserted,
@@ -453,9 +458,9 @@ export const checkpointHistory = (history: History, name: string) => {
...x,
elements: {
...x.elements,
added: stripSeed(x.elements.added),
removed: stripSeed(x.elements.removed),
updated: stripSeed(x.elements.updated),
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
},
})),
).toMatchSnapshot(`[${name}] undo stack`);
@@ -465,9 +470,9 @@ export const checkpointHistory = (history: History, name: string) => {
...x,
elements: {
...x.elements,
added: stripSeed(x.elements.added),
removed: stripSeed(x.elements.removed),
updated: stripSeed(x.elements.updated),
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
},
})),
).toMatchSnapshot(`[${name}] redo stack`);
+3
View File
@@ -813,6 +813,9 @@ export interface ExcalidrawImperativeAPI {
getSceneElementsIncludingDeleted: InstanceType<
typeof App
>["getSceneElementsIncludingDeleted"];
getSceneElementsMapIncludingDeleted: InstanceType<
typeof App
>["getSceneElementsMapIncludingDeleted"];
history: {
clear: InstanceType<typeof App>["resetHistory"];
};