[Editor] Simplify the command manager

The previous version was maybe functional but definitely painful to maintain
(maybe more efficient... I don't know) so this patch aims to simplify it and
it adds some basic unit tests.
This commit is contained in:
Calixte Denizet 2022-07-21 18:31:09 +02:00
parent 5e7eab4dd8
commit af41a5cb49
2 changed files with 127 additions and 76 deletions

View File

@ -54,21 +54,27 @@ class IdManager {
class CommandManager {
#commands = [];
#maxSize = 100;
#maxSize;
// When the position is NaN, it means the buffer is empty.
#position = NaN;
#position = -1;
#start = 0;
constructor(maxSize = 128) {
this.#maxSize = maxSize;
}
/**
* @typedef {Object} addOptions
* @property {function} cmd
* @property {function} undo
* @property {boolean} mustExec
* @property {number} type
* @property {boolean} overwriteIfSameType
* @property {boolean} keepUndo
*/
/**
* Add a new couple of commands to be used in case of redo/undo.
* @param {function} cmd
* @param {function} undo
* @param {boolean} mustExec
* @param {number} type
* @param {boolean} overwriteIfSameType
* @param {boolean} keepUndo
* @param {addOptions} options
*/
add({
cmd,
@ -78,12 +84,18 @@ class CommandManager {
overwriteIfSameType = false,
keepUndo = false,
}) {
if (mustExec) {
cmd();
}
const save = { cmd, undo, type };
if (
overwriteIfSameType &&
!isNaN(this.#position) &&
this.#commands[this.#position].type === type
) {
if (this.#position === -1) {
this.#position = 0;
this.#commands.push(save);
return;
}
if (overwriteIfSameType && this.#commands[this.#position].type === type) {
// For example when we change a color we don't want to
// be able to undo all the steps, hence we only want to
// keep the last undoable action in this sequence of actions.
@ -91,62 +103,41 @@ class CommandManager {
save.undo = this.#commands[this.#position].undo;
}
this.#commands[this.#position] = save;
if (mustExec) {
cmd();
}
return;
}
const next = (this.#position + 1) % this.#maxSize;
if (next !== this.#start) {
if (this.#start < next) {
this.#commands = this.#commands.slice(this.#start, next);
} else {
this.#commands = this.#commands
.slice(this.#start)
.concat(this.#commands.slice(0, next));
}
this.#start = 0;
this.#position = this.#commands.length - 1;
}
this.#setCommands(save);
if (mustExec) {
cmd();
const next = this.#position + 1;
if (next === this.#maxSize) {
this.#commands.splice(0, 1);
} else {
this.#position = next;
if (next < this.#commands.length) {
this.#commands.splice(next);
}
}
this.#commands.push(save);
}
/**
* Undo the last command.
*/
undo() {
if (isNaN(this.#position)) {
if (this.#position === -1) {
// Nothing to undo.
return;
}
this.#commands[this.#position].undo();
if (this.#position === this.#start) {
this.#position = NaN;
} else {
this.#position = (this.#maxSize + this.#position - 1) % this.#maxSize;
}
this.#position -= 1;
}
/**
* Redo the last command.
*/
redo() {
if (isNaN(this.#position)) {
if (this.#start < this.#commands.length) {
this.#commands[this.#start].cmd();
this.#position = this.#start;
}
return;
}
const next = (this.#position + 1) % this.#maxSize;
if (next !== this.#start && next < this.#commands.length) {
this.#commands[next].cmd();
this.#position = next;
if (this.#position < this.#commands.length - 1) {
this.#position += 1;
this.#commands[this.#position].cmd();
}
}
@ -155,7 +146,7 @@ class CommandManager {
* @returns {boolean}
*/
hasSomethingToUndo() {
return !isNaN(this.#position);
return this.#position !== -1;
}
/**
@ -163,29 +154,7 @@ class CommandManager {
* @returns {boolean}
*/
hasSomethingToRedo() {
if (isNaN(this.#position) && this.#start < this.#commands.length) {
return true;
}
const next = (this.#position + 1) % this.#maxSize;
return next !== this.#start && next < this.#commands.length;
}
#setCommands(cmds) {
if (this.#commands.length < this.#maxSize) {
this.#commands.push(cmds);
this.#position = isNaN(this.#position) ? 0 : this.#position + 1;
return;
}
if (isNaN(this.#position)) {
this.#position = this.#start;
} else {
this.#position = (this.#position + 1) % this.#maxSize;
if (this.#position === this.#start) {
this.#start = (this.#start + 1) % this.#maxSize;
}
}
this.#commands[this.#position] = cmds;
return this.#position < this.#commands.length - 1;
}
destroy() {
@ -969,4 +938,10 @@ class AnnotationEditorUIManager {
}
}
export { AnnotationEditorUIManager, bindEvents, ColorManager, KeyboardManager };
export {
AnnotationEditorUIManager,
bindEvents,
ColorManager,
CommandManager,
KeyboardManager,
};

View File

@ -13,9 +13,85 @@
* limitations under the License.
*/
import { CommandManager } from "../../src/display/editor/tools.js";
import { fitCurve } from "../../src/display/editor/ink.js";
describe("editor", function () {
describe("Command Manager", function () {
it("should check undo/redo", function () {
const manager = new CommandManager(4);
let x = 0;
const makeDoUndo = n => ({ cmd: () => (x += n), undo: () => (x -= n) });
manager.add({ ...makeDoUndo(1), mustExec: true });
expect(x).toEqual(1);
manager.add({ ...makeDoUndo(2), mustExec: true });
expect(x).toEqual(3);
manager.add({ ...makeDoUndo(3), mustExec: true });
expect(x).toEqual(6);
manager.undo();
expect(x).toEqual(3);
manager.undo();
expect(x).toEqual(1);
manager.undo();
expect(x).toEqual(0);
manager.undo();
expect(x).toEqual(0);
manager.redo();
expect(x).toEqual(1);
manager.redo();
expect(x).toEqual(3);
manager.redo();
expect(x).toEqual(6);
manager.redo();
expect(x).toEqual(6);
manager.undo();
expect(x).toEqual(3);
manager.redo();
expect(x).toEqual(6);
});
});
it("should hit the limit of the manager", function () {
const manager = new CommandManager(3);
let x = 0;
const makeDoUndo = n => ({ cmd: () => (x += n), undo: () => (x -= n) });
manager.add({ ...makeDoUndo(1), mustExec: true }); // 1
manager.add({ ...makeDoUndo(2), mustExec: true }); // 3
manager.add({ ...makeDoUndo(3), mustExec: true }); // 6
manager.add({ ...makeDoUndo(4), mustExec: true }); // 10
expect(x).toEqual(10);
manager.undo();
manager.undo();
expect(x).toEqual(3);
manager.undo();
expect(x).toEqual(1);
manager.undo();
expect(x).toEqual(1);
manager.redo();
manager.redo();
expect(x).toEqual(6);
manager.add({ ...makeDoUndo(5), mustExec: true });
expect(x).toEqual(11);
});
describe("fitCurve", function () {
it("should return a function", function () {
expect(typeof fitCurve).toEqual("function");