Anki/ts/routes/image-occlusion/tools/tool-undo-redo.ts
RumovZ 790c52f012
Svg icon (#3135)
* Add sveltekit-svg plugin to fix svg icon styling

Closes #3127.

* Unify svg icon usage

Moves all icons into ts/lib/components/icons.ts and uses a single component to render
them both with eslint and svelte-kit.

* Fix spinning revert icon not being centered

* Use svg earth icon for global label

* Add tooltip to global label icon

* Remove eslint-plugin-simple-import-sort

Imports are already sorted by dprint with conflicting rules.
2024-04-24 02:37:31 +01:00

162 lines
4.1 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@generated/ftl";
import { fabric } from "fabric";
import { writable } from "svelte/store";
import { mdiRedo, mdiUndo } from "$lib/components/icons";
import { emitChangeSignal } from "../MaskEditor.svelte";
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
/**
* Undo redo for rectangle and ellipse handled here,
* view tool-polygon for handling undo redo in case of polygon
*/
type UndoState = {
undoable: boolean;
redoable: boolean;
};
const shapeType = ["rect", "ellipse", "i-text", "group"];
const validShape = (shape: fabric.Object): boolean => {
if (shape.width! <= 5 || shape.height! <= 5) {
return false;
}
if (shapeType.indexOf(shape.type!) === -1) {
return false;
}
return true;
};
class UndoStack {
private stack: string[] = [];
private index = -1;
private canvas: fabric.Canvas | undefined;
private locked = false;
private shapeIds = new Set<string>();
/** used to make the toolbar buttons reactive */
private state = writable<UndoState>({ undoable: false, redoable: false });
subscribe: typeof this.state.subscribe;
constructor() {
// allows an instance of the class to act as a store
this.subscribe = this.state.subscribe;
}
setCanvas(canvas: fabric.Canvas): void {
this.canvas = canvas;
this.canvas.on("object:modified", (opts) => this.maybePush(opts));
this.canvas.on("object:removed", (opts) => {
if (!opts.target!.group && !opts.target!.destroyed) {
this.maybePush(opts);
}
});
}
reset(): void {
this.shapeIds.clear();
this.stack.length = 0;
this.index = -1;
this.push();
this.updateState();
}
private canUndo(): boolean {
return this.index > 0;
}
private canRedo(): boolean {
return this.index < this.stack.length - 1;
}
private updateState(): void {
this.state.set({
undoable: this.canUndo(),
redoable: this.canRedo(),
});
}
private updateCanvas(): void {
this.locked = true;
this.canvas?.loadFromJSON(this.stack[this.index], () => {
this.canvas?.renderAll();
emitChangeSignal();
this.locked = false;
});
// make bounding box unselectable
this.canvas?.forEachObject((obj) => {
if (obj instanceof fabric.Rect && obj.fill === "transparent") {
obj.selectable = false;
}
});
}
onObjectAdded(id: string): void {
if (!this.shapeIds.has(id)) {
this.push();
}
this.shapeIds.add(id);
emitChangeSignal();
}
onObjectModified(): void {
this.push();
emitChangeSignal();
}
private maybePush(obj: fabric.IEvent<MouseEvent>): void {
if (!this.locked && validShape(obj.target!)) {
this.push();
}
}
private push(): void {
const entry = JSON.stringify(this.canvas);
if (entry === this.stack[this.stack.length - 1]) {
return;
}
this.stack.length = this.index + 1;
this.stack.push(entry);
this.index++;
this.updateState();
}
undo(): void {
if (this.canUndo()) {
this.index--;
this.updateState();
this.updateCanvas();
}
}
redo(): void {
if (this.canRedo()) {
this.index++;
this.updateState();
this.updateCanvas();
}
}
}
export const undoStack = new UndoStack();
export const undoRedoTools = [
{
name: "undo",
icon: mdiUndo,
action: () => undoStack.undo(),
tooltip: tr.undoUndo,
shortcut: undoKeyCombination,
},
{
name: "redo",
icon: mdiRedo,
action: () => undoStack.redo(),
tooltip: tr.undoRedo,
shortcut: redoKeyCombination,
},
];