fix select all and change ordinal in edit mode in io (#3109)

* fix select all and change ordinal in edit mode in io

* make ordinal undefined for all shapes in group/ungroup

* fix group shapes and some ui fixes

* Don't add node_modules/* to dprint deps

* use minimum ordinal when shape merged, use max ordinal++ when ungrouped, in add mode no ordinal preset so NaN

* use state for ungroup shape

* maintain existing ordinal in editing mode

* fix order of ordinal in ungroup shape

* refactor: remove for loop, use forEach
This commit is contained in:
Mani 2024-04-11 15:08:07 +08:00 committed by GitHub
parent da4551e351
commit 477f932f35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 103 additions and 36 deletions

View file

@ -292,7 +292,7 @@ fn build_and_check_reviewer(build: &mut Build) -> Result<()> {
fn check_web(build: &mut Build) -> Result<()> { fn check_web(build: &mut Build) -> Result<()> {
let dprint_files = inputs![glob![ let dprint_files = inputs![glob![
"**/*.{ts,mjs,js,md,json,toml,svelte,scss}", "**/*.{ts,mjs,js,md,json,toml,svelte,scss}",
"{target,ts/.svelte-kit}/**" "{target,ts/.svelte-kit,node_modules}/**"
]]; ]];
build.add_action( build.add_action(
"check:format:dprint", "check:format:dprint",

View file

@ -471,6 +471,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
font-size: 16px !important; font-size: 16px !important;
} }
:global(.top-tool-icon-button:active) {
background: var(--highlight-bg) !important;
}
.dropdown-content { .dropdown-content {
display: none; display: none;
position: absolute; position: absolute;
@ -480,7 +484,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
.show { .show {
display: flex; display: table;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {

View file

@ -19,23 +19,71 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
const shapes = baseShapesFromFabric(); const shapes = baseShapesFromFabric();
let clozes = ""; let clozes = "";
let index = 0; let noteCount = 0;
shapes.forEach((shapeOrShapes) => {
// shapes with width or height less than 5 are not valid // take out all ordinal values from shapes
if (shapeOrShapes === null) { const ordinalList = shapes.map((shape) => {
return; if (Array.isArray(shape)) {
} return shape[0].ordinal;
// if shape is Rect and fill is transparent, skip it } else {
if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") { return shape.ordinal;
return;
}
clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive);
if (!(shapeOrShapes instanceof Text)) {
index++;
} }
}); });
return { clozes, noteCount: index }; const filterOrdinalList: number[] = ordinalList.flatMap(v => typeof v === "number" ? [v] : []);
const maxOrdinal = Math.max(...filterOrdinalList, 0);
const missingOrdinals: number[] = [];
for (let i = 1; i <= maxOrdinal; i++) {
if (!ordinalList.includes(i)) {
missingOrdinals.push(i);
}
}
let nextOrdinal = maxOrdinal + 1;
shapes.map((shapeOrShapes) => {
if (shapeOrShapes === null) {
return;
}
// Maintain existing ordinal in editing mode
let ordinal: number | undefined;
if (Array.isArray(shapeOrShapes)) {
ordinal = shapeOrShapes[0].ordinal;
} else {
ordinal = shapeOrShapes.ordinal;
}
if (ordinal === undefined) {
// if ordinal is undefined, assign a missing ordinal if available
if (shapeOrShapes instanceof Text) {
ordinal = 0;
} else if (missingOrdinals.length > 0) {
ordinal = missingOrdinals.shift() as number;
} else {
ordinal = nextOrdinal;
nextOrdinal++;
}
if (Array.isArray(shapeOrShapes)) {
shapeOrShapes.forEach((shape) => (shape.ordinal = ordinal));
} else {
shapeOrShapes.ordinal = ordinal;
}
}
clozes += shapeOrShapesToCloze(
shapeOrShapes,
ordinal,
occludeInactive,
);
if (!(shapeOrShapes instanceof Text)) {
noteCount++;
}
});
return { clozes, noteCount };
} }
/** Gather all Fabric shapes, and convert them into BaseShapes or /** Gather all Fabric shapes, and convert them into BaseShapes or
@ -50,6 +98,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
: null; : null;
const objects = canvas.getObjects() as fabric.Object[]; const objects = canvas.getObjects() as fabric.Object[];
const boundingBox = getBoundingBox(); const boundingBox = getBoundingBox();
// filter transparent rectangles
return objects return objects
.map((object) => { .map((object) => {
// If the object is in the active selection containing multiple objects, // If the object is in the active selection containing multiple objects,
@ -57,7 +106,9 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
const parent = selectionContainingMultipleObjects?.contains(object) const parent = selectionContainingMultipleObjects?.contains(object)
? selectionContainingMultipleObjects ? selectionContainingMultipleObjects
: undefined; : undefined;
if (object.width! < 5 || object.height! < 5) { // shapes with width or height less than 5 are not valid
// if shape is Rect and fill is transparent, skip it
if (object.width! < 5 || object.height! < 5 || object.fill == "transparent") {
return null; return null;
} }
return fabricObjectToBaseShapeOrShapes( return fabricObjectToBaseShapeOrShapes(
@ -131,7 +182,7 @@ function fabricObjectToBaseShapeOrShapes(
{{c1::image-occlusion:rect:top=.1:left=.23:width=.4:height=.5}} */ {{c1::image-occlusion:rect:top=.1:left=.23:width=.4:height=.5}} */
function shapeOrShapesToCloze( function shapeOrShapesToCloze(
shapeOrShapes: ShapeOrShapes, shapeOrShapes: ShapeOrShapes,
index: number, ordinal: number,
occludeInactive: boolean, occludeInactive: boolean,
): string { ): string {
let text = ""; let text = "";
@ -143,7 +194,7 @@ function shapeOrShapesToCloze(
let type: string; let type: string;
if (Array.isArray(shapeOrShapes)) { if (Array.isArray(shapeOrShapes)) {
return shapeOrShapes return shapeOrShapes
.map((shape) => shapeOrShapesToCloze(shape, index, occludeInactive)) .map((shape) => shapeOrShapesToCloze(shape, ordinal, occludeInactive))
.join(""); .join("");
} else if (shapeOrShapes instanceof Rectangle) { } else if (shapeOrShapes instanceof Rectangle) {
type = "rect"; type = "rect";
@ -164,16 +215,6 @@ function shapeOrShapesToCloze(
addKeyValue("oi", "1"); 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>`; text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
return text; return text;

View file

@ -63,12 +63,20 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
const activeObject = canvas.getActiveObject() as fabric.ActiveSelection; const activeObject = canvas.getActiveObject() as fabric.ActiveSelection;
const items = activeObject.getObjects(); const items = activeObject.getObjects();
// @ts-expect-error not defined
let minOrdinal: number | undefined = Math.min(...items.map((item) => item.ordinal));
minOrdinal = Number.isNaN(minOrdinal) ? undefined : minOrdinal;
items.forEach((item) => { items.forEach((item) => {
item.set({ opacity: 1 }); // @ts-expect-error not defined
item.set({ opacity: 1, ordinal: minOrdinal });
}); });
activeObject.toGroup().set({ activeObject.toGroup().set({
opacity: get(opacityStateStore) ? 0.4 : 1, opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
redraw(canvas); redraw(canvas);
}; };
@ -84,13 +92,17 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
group._restoreObjectsState(); group._restoreObjectsState();
// @ts-expect-error not defined // @ts-expect-error not defined
group.destroyed = true; group.destroyed = true;
canvas.remove(group);
items.forEach((item) => { items.forEach((item) => {
item.set({ opacity: get(opacityStateStore) ? 0.4 : 1 }); item.set({
opacity: get(opacityStateStore) ? 0.4 : 1,
// @ts-expect-error not defined
ordinal: undefined,
});
canvas.add(item); canvas.add(item);
}); });
canvas.remove(group);
redraw(canvas); redraw(canvas);
}; };
@ -283,9 +295,13 @@ export const makeShapeRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fabr
export const selectAllShapes = (canvas: fabric.Canvas) => { export const selectAllShapes = (canvas: fabric.Canvas) => {
canvas.discardActiveObject(); canvas.discardActiveObject();
const sel = new fabric.ActiveSelection(canvas.getObjects(), { // filter out the transparent bounding box from the selection
canvas: canvas, const sel = new fabric.ActiveSelection(
}); canvas.getObjects().filter((obj) => obj.fill !== "transparent"),
{
canvas: canvas,
},
);
canvas.setActiveObject(sel); canvas.setActiveObject(sel);
redraw(canvas); redraw(canvas);
}; };

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@generated/ftl"; import * as tr from "@generated/ftl";
import { type fabric } from "fabric"; import { fabric } from "fabric";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { mdiRedo, mdiUndo } from "../icons"; import { mdiRedo, mdiUndo } from "../icons";
@ -87,6 +87,12 @@ class UndoStack {
emitChangeSignal(); emitChangeSignal();
this.locked = false; 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 { onObjectAdded(id: string): void {