[web] Undo-Redo 撤销重做机制——BabylonJS-Editor

1,118 阅读2分钟

题记:

如果需要一个撤销、重做的机制可以看看这篇文章。

记录一下BabylonJS 编辑器项目中的Undo-Redo撤销重做机制。

BabylonJS Editor源码地址:>github.com/BabylonJS/E…

一、如何实现

定义一个指令类,包含:撤销、重置、回调等函数体。定义一个工具栏,声明指令栈stack,在合适的地方塞入指令、弹出指令即可。

  • IUndoRedoAction:UndoRedo的行为指令

    • common: 当新增指令、撤销指令、重置指令时执行的一些行为函数
    • undo:撤销行为函数
    • redo:重做行为函数
/**
 * Defines the type used to define the return type of undo/redo actions.
 */
export type UndoRedoReturnType<T> = (T | void) | (Promise<T> | Promise<void>);

export interface IUndoRedoAction {
	/**
	 * Defines the description of the undo/redo-able action that has been performed.
	 */
	description?: string;
	/**
	 * Defines the callback called on an undo or redo action has been performed. This
	 * is typically used to perform an action in both cases (undo and redo).
	 */
	common?: (step: "push" | "redo" | "undo") => UndoRedoReturnType<unknown>;
	/**
	 * Defines the callback called on an action should be undone.
	 */
	undo: () => UndoRedoReturnType<unknown>;
	/**
	 * Defines the callback called on an action should be redone.
	 * Calling undoRedo.push(...) will automatically call this callback.
	 */
	redo: () => UndoRedoReturnType<unknown>;
}

定义操作指令的工具类 维护一个 stack,将指令行为进行管理.

export class UndoRedo {
	public _position: number = -1;
	private _stack: IUndoRedoAction[] = [];

	/**
	 * Gets the reference to the current stack of actions.
	 */
	public get stack(): ReadonlyArray<IUndoRedoAction> {
		return this._stack;
	}

	/**
	 * Pushes the given element to the current undo/redo stack. If the current action index
	 * is inferior to the stack size then the stack will be broken.
	 * @param element defines the reference to the element to push in the undo/redo stack.
	 */
	public push<T>(element: IUndoRedoAction): UndoRedoReturnType<T> {
		// Check index
		if (this._position < this._stack.length - 1) {
			this._stack.splice(this._position + 1);
		}

		// Push element and call the redo function
		this._stack.push(element);
		return this._redo("push");
	}

	/**
	 * Undoes the action located at the current index of the stack.
	 * If the action is asynchronous, its promise is returned.
	 */
	public undo<T>(): UndoRedoReturnType<T> {
		return this._undo();
	}

	/**
	 * Redoes the current action located at the current index of the stack.
	 * If the action is asynchronous, its promise is returned.
	 */
	public redo<T>(): UndoRedoReturnType<T> {
		return this._redo("redo");
	}

	/**
	 * Called on an undo action should be performed.
	 */
	private _undo<T>(): UndoRedoReturnType<T> {
		if (this._position < 0) {
			return shell.beep();
		}

		const element = this._stack[this._position];

		const possiblePromise = element.undo();
		if (possiblePromise instanceof Promise) {
			possiblePromise.then(() => {
				element.common?.("undo");
			});
		} else {
			element.common?.("undo");
		}

		this._position--;
		return possiblePromise as UndoRedoReturnType<T>;
	}

	/**
	 * Called on a redo action should be performed.
	 */
	private _redo<T>(step: "push""redo"): UndoRedoReturnType<T> {
		if (this._position >= this._stack.length - 1) {
			return shell.beep();
		}

		this._position++;

		const element = this._stack[this._position];
		const possiblePromise = element.redo();
		if (possiblePromise instanceof Promise) {
			possiblePromise.then(() => {
				element.common?.(step);
			});
		} else {
			element.common?.(step);
		}

		return possiblePromise as UndoRedoReturnType<T>;
	}

    /**
     * Clears the current undo/redo stack.
     */
    public clear(): void {
        this._stack = [];
        this._position = -1;
    }
}


二、使用

我们需要检测的对象发生改变时,加入一个撤销重做行为到指令栈


undoRedo.push({
    description: `Changed object transform "${attachedMesh.name}" from "${endValue.toString()}" to "${initialValue.toString()}"`,
    common: () => console.log("操作指令")),
    redo: () => console.log("执行了重置操作"),
    undo: () => console.log("执行了撤销操作"),
});

然后在工具栏加一个撤销重做的按钮 或者检测crtl+z ,和ctrl+shift+z 绑定执行undo 和redo的方法。

  ...
     switch(action){
      // Edit
        case Action.Undo: undoRedo.undo(); break;
        case Action.Redo: undoRedo.redo(); break;
     }
  ...

哈哈哈哈 一个简单的undo、redo机制。不过适用于很多场景了。在这个基础上我们还可以为指令进行额外扩展。

如何自动化检测添加行为呢?

方案可参考使用:Proxy、immutable.js

下期预告: BabylonJS-Editor编辑器的设置面板自动化配置原理。

4/300