diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 325090d3de0..0b432738d32 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -458,7 +458,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/**" + "{target,ts/.svelte-kit,node_modules}/**" ]]; build.add_action( "check:format:dprint", diff --git a/ftl/core-repo b/ftl/core-repo index c74c15b7f82..e3af3c98324 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit c74c15b7f82c0f184910e5b6f695b635e6d81faf +Subproject commit e3af3c983241448a239871ca573c9dd2fa5e8619 diff --git a/ftl/qt-repo b/ftl/qt-repo index 06ad12df7a2..45155310c33 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit 06ad12df7a2c8400cf64e9c7b986e9ee722e5b38 +Subproject commit 45155310c3302cbbbe645dec52ca196894422463 diff --git a/ts/image-occlusion/Toolbar.svelte b/ts/image-occlusion/Toolbar.svelte index 7cfe0eeaa55..af84ffe667b 100644 --- a/ts/image-occlusion/Toolbar.svelte +++ b/ts/image-occlusion/Toolbar.svelte @@ -470,6 +470,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; @@ -479,7 +483,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/image-occlusion/shapes/to-cloze.ts b/ts/image-occlusion/shapes/to-cloze.ts index 8b0b53bf4b0..39bf7989ce1 100644 --- a/ts/image-occlusion/shapes/to-cloze.ts +++ b/ts/image-occlusion/shapes/to-cloze.ts @@ -20,23 +20,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 + 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; + } + }); + + 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; } - // if shape is Rect and fill is transparent, skip it - if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") { - return; + + // Maintain existing ordinal in editing mode + let ordinal: number | undefined; + if (Array.isArray(shapeOrShapes)) { + ordinal = shapeOrShapes[0].ordinal; + } else { + ordinal = shapeOrShapes.ordinal; } - clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive); + + 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)) { - index++; + noteCount++; } }); - - return { clozes, noteCount: index }; + return { clozes, noteCount }; } /** Gather all Fabric shapes, and convert them into BaseShapes or @@ -51,6 +99,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] { : null; const objects = canvas.getObjects() as FabricObject[]; const boundingBox = getBoundingBox(); + // filter transparent rectangles return objects .map((object) => { // If the object is in the active selection containing multiple objects, @@ -58,7 +107,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( @@ -132,7 +183,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 = ""; @@ -144,7 +195,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"; @@ -165,16 +216,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/image-occlusion/tools/lib.ts b/ts/image-occlusion/tools/lib.ts index d5603e5709b..2d798996877 100644 --- a/ts/image-occlusion/tools/lib.ts +++ b/ts/image-occlusion/tools/lib.ts @@ -64,12 +64,18 @@ export const groupShapes = (canvas: fabric.Canvas): void => { const activeObject = canvas.getActiveObject(); const items = activeObject.getObjects(); + + let minOrdinal: number | undefined = Math.min(...items.map((item) => item.ordinal)); + minOrdinal = Number.isNaN(minOrdinal) ? undefined : minOrdinal; + items.forEach((item) => { - item.set({ opacity: 1 }); + item.set({ opacity: 1, ordinal: minOrdinal }); }); + activeObject.toGroup().set({ opacity: get(opacityStateStore) ? 0.4 : 1, }); + redraw(canvas); }; @@ -85,13 +91,16 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => { const items = group.getObjects(); group._restoreObjectsState(); 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, + ordinal: undefined, + }); canvas.add(item); }); + canvas.remove(group); redraw(canvas); }; @@ -282,9 +291,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/image-occlusion/tools/tool-undo-redo.ts b/ts/image-occlusion/tools/tool-undo-redo.ts index 13af962f11c..3a179705c63 100644 --- a/ts/image-occlusion/tools/tool-undo-redo.ts +++ b/ts/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 "@tslib/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 {