diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index f02bd7a47..4d7b744a7 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -292,7 +292,7 @@ fn build_and_check_reviewer(build: &mut Build) -> Result<()> { fn check_web(build: &mut Build) -> Result<()> { let dprint_files = inputs![glob![ "**/*.{ts,mjs,js,md,json,toml,svelte,scss}", - "{target,ts/.svelte-kit}/**" + "{target,ts/.svelte-kit,node_modules}/**" ]]; build.add_action( "check:format:dprint", diff --git a/ts/routes/image-occlusion/Toolbar.svelte b/ts/routes/image-occlusion/Toolbar.svelte index 2e00f64b9..8941ab39a 100644 --- a/ts/routes/image-occlusion/Toolbar.svelte +++ b/ts/routes/image-occlusion/Toolbar.svelte @@ -471,6 +471,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html font-size: 16px !important; } + :global(.top-tool-icon-button:active) { + background: var(--highlight-bg) !important; + } + .dropdown-content { display: none; position: absolute; @@ -480,7 +484,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .show { - display: flex; + display: table; } ::-webkit-scrollbar { diff --git a/ts/routes/image-occlusion/shapes/to-cloze.ts b/ts/routes/image-occlusion/shapes/to-cloze.ts index ce4dfacbb..521666f60 100644 --- a/ts/routes/image-occlusion/shapes/to-cloze.ts +++ b/ts/routes/image-occlusion/shapes/to-cloze.ts @@ -19,23 +19,71 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): { 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++; + let noteCount = 0; + + // take out all ordinal values from shapes + const ordinalList = shapes.map((shape) => { + if (Array.isArray(shape)) { + return shape[0].ordinal; + } else { + return shape.ordinal; } }); - 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 @@ -50,6 +98,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] { : null; const objects = canvas.getObjects() as fabric.Object[]; const boundingBox = getBoundingBox(); + // filter transparent rectangles return objects .map((object) => { // If the object is in the active selection containing multiple objects, @@ -57,7 +106,9 @@ export function baseShapesFromFabric(): ShapeOrShapes[] { const parent = selectionContainingMultipleObjects?.contains(object) ? selectionContainingMultipleObjects : 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 fabricObjectToBaseShapeOrShapes( @@ -131,7 +182,7 @@ function fabricObjectToBaseShapeOrShapes( {{c1::image-occlusion:rect:top=.1:left=.23:width=.4:height=.5}} */ function shapeOrShapesToCloze( shapeOrShapes: ShapeOrShapes, - index: number, + ordinal: number, occludeInactive: boolean, ): string { let text = ""; @@ -143,7 +194,7 @@ function shapeOrShapesToCloze( let type: string; if (Array.isArray(shapeOrShapes)) { return shapeOrShapes - .map((shape) => shapeOrShapesToCloze(shape, index, occludeInactive)) + .map((shape) => shapeOrShapesToCloze(shape, ordinal, occludeInactive)) .join(""); } else if (shapeOrShapes instanceof Rectangle) { type = "rect"; @@ -164,16 +215,6 @@ function shapeOrShapesToCloze( 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}}}
`; return text; diff --git a/ts/routes/image-occlusion/tools/lib.ts b/ts/routes/image-occlusion/tools/lib.ts index 19543c26f..953ac290e 100644 --- a/ts/routes/image-occlusion/tools/lib.ts +++ b/ts/routes/image-occlusion/tools/lib.ts @@ -63,12 +63,20 @@ export const groupShapes = (canvas: fabric.Canvas): void => { const activeObject = canvas.getActiveObject() as fabric.ActiveSelection; 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) => { - item.set({ opacity: 1 }); + // @ts-expect-error not defined + item.set({ opacity: 1, ordinal: minOrdinal }); }); + activeObject.toGroup().set({ opacity: get(opacityStateStore) ? 0.4 : 1, }); + redraw(canvas); }; @@ -84,13 +92,17 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => { group._restoreObjectsState(); // @ts-expect-error not defined group.destroyed = true; - canvas.remove(group); 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.remove(group); redraw(canvas); }; @@ -283,9 +295,13 @@ export const makeShapeRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fabr export const selectAllShapes = (canvas: fabric.Canvas) => { canvas.discardActiveObject(); - const sel = new fabric.ActiveSelection(canvas.getObjects(), { - canvas: canvas, - }); + // filter out the transparent bounding box from the selection + const sel = new fabric.ActiveSelection( + canvas.getObjects().filter((obj) => obj.fill !== "transparent"), + { + canvas: canvas, + }, + ); canvas.setActiveObject(sel); redraw(canvas); }; diff --git a/ts/routes/image-occlusion/tools/tool-undo-redo.ts b/ts/routes/image-occlusion/tools/tool-undo-redo.ts index 0a8b91108..aa073dfe4 100644 --- a/ts/routes/image-occlusion/tools/tool-undo-redo.ts +++ b/ts/routes/image-occlusion/tools/tool-undo-redo.ts @@ -2,7 +2,7 @@ // 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 { fabric } from "fabric"; import { writable } from "svelte/store"; import { mdiRedo, mdiUndo } from "../icons"; @@ -87,6 +87,12 @@ class UndoStack { 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 {