feat: multiplayer undo / redo (#7348)

This commit is contained in:
Marcel Mraz 2024-04-17 13:01:24 +01:00 committed by GitHub
parent 5211b003b8
commit 530617be90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 34885 additions and 14877 deletions

File diff suppressed because it is too large Load diff

View file

@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 745419401,
"versionNonce": 1984422985,
"width": 300,
"x": 201,
"y": 2,
@ -186,7 +186,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id1",
"focus": -0.46666666666666673,
@ -227,10 +227,10 @@ exports[`move element > rectangles with binding arrow 7`] = `
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "line",
"type": "arrow",
"updated": 1,
"version": 12,
"versionNonce": 1984422985,
"version": 14,
"versionNonce": 2066753033,
"width": 81,
"x": 110,
"y": 49.981789081137734,

View file

@ -27,7 +27,7 @@ const checkpoint = (name: string) => {
`[${name}] number of renders`,
);
expect(h.state).toMatchSnapshot(`[${name}] appState`);
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
expect(h.history).toMatchSnapshot(`[${name}] history`);
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
h.elements.forEach((element, i) =>
expect(element).toMatchSnapshot(`[${name}] element ${i}`),

View file

@ -69,9 +69,18 @@ export class API {
return selectedElements[0];
};
static getStateHistory = () => {
static getUndoStack = () => {
// @ts-ignore
return h.history.stateHistory;
return h.history.undoStack;
};
static getRedoStack = () => {
// @ts-ignore
return h.history.redoStack;
};
static getSnapshot = () => {
return Array.from(h.store.snapshot.elements.values());
};
static clearSelection = () => {

View file

@ -113,6 +113,18 @@ export class Keyboard {
Keyboard.codeDown(code);
Keyboard.codeUp(code);
};
static undo = () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress("z");
});
};
static redo = () => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress("z");
});
};
}
const getElementPointForSelection = (element: ExcalidrawElement): Point => {

File diff suppressed because it is too large Load diff

View file

@ -77,30 +77,30 @@ describe("move element", () => {
// create elements
const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const line = UI.createElement("line", { x: 110, y: 50, size: 80 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
const elementsMap = h.app.scene.getNonDeletedElementsMap();
// bind line to two rectangles
bindOrUnbindLinearElement(
line.get() as NonDeleted<ExcalidrawLinearElement>,
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement,
elementsMap,
);
// select the second rectangles
// select the second rectangle
new Pointer("mouse").clickOn(rectB);
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`21`,
`20`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`19`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`17`);
expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(3);
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([200, 0]);
expect([line.x, line.y]).toEqual([110, 50]);
expect([line.width, line.height]).toEqual([80, 80]);
expect([arrow.x, arrow.y]).toEqual([110, 50]);
expect([arrow.width, arrow.height]).toEqual([80, 80]);
renderInteractiveScene.mockClear();
renderStaticScene.mockClear();
@ -118,8 +118,10 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]);
expect([Math.round(line.x), Math.round(line.y)]).toEqual([110, 50]);
expect([Math.round(line.width), Math.round(line.height)]).toEqual([81, 81]);
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([110, 50]);
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
81, 81,
]);
h.elements.forEach((element) => expect(element).toMatchSnapshot());
});

View file

@ -35,7 +35,7 @@ const checkpoint = (name: string) => {
`[${name}] number of renders`,
);
expect(h.state).toMatchSnapshot(`[${name}] appState`);
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
expect(h.history).toMatchSnapshot(`[${name}] history`);
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
h.elements.forEach((element, i) =>
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
@ -359,6 +359,7 @@ describe("regression tests", () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
Keyboard.keyPress(KEYS.Z);
Keyboard.keyPress(KEYS.Z);
});
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
Keyboard.withModifierKeys({ ctrl: true }, () => {
@ -372,7 +373,7 @@ describe("regression tests", () => {
});
it("noop interaction after undo shouldn't create history entry", () => {
expect(API.getStateHistory().length).toBe(1);
expect(API.getUndoStack().length).toBe(0);
UI.clickTool("rectangle");
mouse.down(10, 10);
@ -386,35 +387,35 @@ describe("regression tests", () => {
const secondElementEndPoint = mouse.getPosition();
expect(API.getStateHistory().length).toBe(3);
expect(API.getUndoStack().length).toBe(2);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
});
expect(API.getStateHistory().length).toBe(2);
expect(API.getUndoStack().length).toBe(1);
// clicking an element shouldn't add to history
mouse.restorePosition(...firstElementEndPoint);
mouse.click();
expect(API.getStateHistory().length).toBe(2);
expect(API.getUndoStack().length).toBe(1);
Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
});
expect(API.getStateHistory().length).toBe(3);
expect(API.getUndoStack().length).toBe(2);
// clicking an element shouldn't add to history
// clicking an element should add to history
mouse.click();
expect(API.getStateHistory().length).toBe(3);
expect(API.getUndoStack().length).toBe(3);
const firstSelectedElementId = API.getSelectedElement().id;
// same for clicking the element just redo-ed
mouse.restorePosition(...secondElementEndPoint);
mouse.click();
expect(API.getStateHistory().length).toBe(3);
expect(API.getUndoStack().length).toBe(4);
expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId);
});