Anki/ts/routes/image-occlusion/shapes/to-cloze.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

180 lines
5.6 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { fabric } from "fabric";
import { cloneDeep } from "lodash-es";
import { getBoundingBox } from "../tools/lib";
import type { Size } from "../types";
import type { Shape, ShapeOrShapes } from "./base";
import { Ellipse } from "./ellipse";
import { Polygon } from "./polygon";
import { Rectangle } from "./rectangle";
import { Text } from "./text";
export function exportShapesToClozeDeletions(occludeInactive: boolean): {
clozes: string;
noteCount: number;
} {
const shapes = baseShapesFromFabric();
let clozes = "";
let index = 0;
shapes.forEach((shapeOrShapes) => {
// shapes with width or height less than 5 are not valid
if (shapeOrShapes === null) {
return;
}
// if shape is Rect and fill is transparent, skip it
if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") {
return;
}
clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive);
if (!(shapeOrShapes instanceof Text)) {
index++;
}
});
return { clozes, noteCount: index };
}
/** Gather all Fabric shapes, and convert them into BaseShapes or
* BaseShape[]s.
*/
export function baseShapesFromFabric(): ShapeOrShapes[] {
const canvas = globalThis.canvas as fabric.Canvas;
const activeObject = canvas.getActiveObject();
const selectionContainingMultipleObjects = activeObject instanceof fabric.ActiveSelection
&& (activeObject.size() > 1)
? activeObject
: null;
const objects = canvas.getObjects() as fabric.Object[];
const boundingBox = getBoundingBox();
return objects
.map((object) => {
// If the object is in the active selection containing multiple objects,
// we need to calculate its x and y coordinates relative to the canvas.
const parent = selectionContainingMultipleObjects?.contains(object)
? selectionContainingMultipleObjects
: undefined;
if (object.width! < 5 || object.height! < 5) {
return null;
}
return fabricObjectToBaseShapeOrShapes(
boundingBox,
object,
parent,
);
})
.filter((o): o is ShapeOrShapes => o !== null);
}
/** Convert a single Fabric object/group to one or more BaseShapes. */
function fabricObjectToBaseShapeOrShapes(
size: Size,
object: fabric.Object,
parentObject?: fabric.Object,
): ShapeOrShapes | null {
let shape: Shape;
// Prevents the original fabric object from mutating when a non-primitive
// property of a Shape mutates.
const cloned = cloneDeep(object);
if (parentObject) {
const scaling = parentObject.getObjectScaling();
cloned.width = cloned.width! * scaling.scaleX;
cloned.height = cloned.height! * scaling.scaleY;
}
switch (object.type) {
case "rect":
shape = new Rectangle(cloned as any);
break;
case "ellipse":
shape = new Ellipse(cloned as any);
break;
case "polygon":
shape = new Polygon(cloned as any);
break;
case "i-text":
shape = new Text(cloned as any);
break;
case "group":
return (object as fabric.Group).getObjects().flatMap((child) => {
return fabricObjectToBaseShapeOrShapes(
size,
child,
object,
)!;
});
default:
return null;
}
if (parentObject) {
const newPosition = fabric.util.transformPoint(
new fabric.Point(shape.left, shape.top),
parentObject.calcTransformMatrix(),
);
shape.left = newPosition.x;
shape.top = newPosition.y;
}
if (size == undefined) {
size = { width: 0, height: 0 };
}
shape = shape.toNormal(size);
return shape;
}
/** generate cloze data in form of
{{c1::image-occlusion:rect:top=.1:left=.23:width=.4:height=.5}} */
function shapeOrShapesToCloze(
shapeOrShapes: ShapeOrShapes,
index: number,
occludeInactive: boolean,
): string {
let text = "";
function addKeyValue(key: string, value: string) {
value = value.replace(":", "\\:");
text += `:${key}=${value}`;
}
let type: string;
if (Array.isArray(shapeOrShapes)) {
return shapeOrShapes
.map((shape) => shapeOrShapesToCloze(shape, index, occludeInactive))
.join("");
} else if (shapeOrShapes instanceof Rectangle) {
type = "rect";
} else if (shapeOrShapes instanceof Ellipse) {
type = "ellipse";
} else if (shapeOrShapes instanceof Polygon) {
type = "polygon";
} else if (shapeOrShapes instanceof Text) {
type = "text";
} else {
throw new Error("Unknown shape type");
}
for (const [key, value] of Object.entries(shapeOrShapes.toDataForCloze())) {
addKeyValue(key, value);
}
if (occludeInactive) {
addKeyValue("oi", "1");
}
// Maintain existing ordinal in editing mode
let ordinal = shapeOrShapes.ordinal;
if (ordinal === undefined) {
if (type === "text") {
ordinal = 0;
} else {
ordinal = index + 1;
}
shapeOrShapes.ordinal = ordinal;
}
text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
return text;
}