Anki/ts/routes/image-occlusion/tools/tool-undo-redo.ts
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

156 lines
4 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 { type fabric } from "fabric";
import { writable } from "svelte/store";
import { mdiRedo, mdiUndo } from "../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) => {
// @ts-expect-error `destroyed` is a custom property set on groups in the ungrouping routine to avoid adding a spurious undo entry
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;
});
}
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,
},
];