From 33d1057a46cf327141ddc87ff5fc552ea3cf22db Mon Sep 17 00:00:00 2001 From: jariji Date: Mon, 3 Nov 2025 22:32:26 +0000 Subject: [PATCH 1/3] Add "hide all but one" occlusion mode. This PR adds the "hide all but one" occlusion mode. An example use case is a note containing a collection of pairs of selection, where each selection is the prompt for the other in its pair. For example, given a table like | small | big | |-------+-----| | a | A | | b | B | | c | C | in each card, five letters are occluded, and one is shown. The user is prompted to state the occluded symbol that is adjacent to the shown symbol. --- ftl/core/notetypes.ftl | 1 + proto/anki/image_occlusion.proto | 8 +- rslib/src/image_occlusion/imagedata.rs | 23 ++++-- ts/editor/NoteEditor.svelte | 4 +- ts/lib/components/icons.ts | 3 + ts/routes/image-occlusion/Toolbar.svelte | 34 +++++--- .../[...imagePathOrNoteId]/+page.ts | 4 +- .../add-or-update-note.svelte.ts | 6 +- ts/routes/image-occlusion/index.ts | 4 +- ts/routes/image-occlusion/mask-editor.ts | 4 +- ts/routes/image-occlusion/review.ts | 80 ++++++++++++------- ts/routes/image-occlusion/shapes/base.ts | 10 +-- .../image-occlusion/shapes/from-cloze.ts | 2 +- ts/routes/image-occlusion/shapes/to-cloze.ts | 13 +-- ts/routes/image-occlusion/store.ts | 10 ++- 15 files changed, 133 insertions(+), 73 deletions(-) diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl index f7eccd08b..03159ba98 100644 --- a/ftl/core/notetypes.ftl +++ b/ftl/core/notetypes.ftl @@ -47,6 +47,7 @@ notetypes-toggle-masks = Toggle Masks notetypes-image-occlusion-name = Image Occlusion notetypes-hide-all-guess-one = Hide All, Guess One notetypes-hide-one-guess-one = Hide One, Guess One +notetypes-hide-all-but-one = Hide All But One notetypes-error-generating-cloze = An error occurred when generating an image occlusion note notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date? diff --git a/proto/anki/image_occlusion.proto b/proto/anki/image_occlusion.proto index edbfe9ffd..f54206131 100644 --- a/proto/anki/image_occlusion.proto +++ b/proto/anki/image_occlusion.proto @@ -69,6 +69,12 @@ message GetImageOcclusionNoteResponse { uint32 ordinal = 2; } + enum OcclusionMode { + HIDE_ONE = 0; + HIDE_ALL = 1; + HIDE_ALL_BUT_ONE = 2; + } + message ImageOcclusionNote { bytes image_data = 1; repeated ImageOcclusion occlusions = 2; @@ -76,7 +82,7 @@ message GetImageOcclusionNoteResponse { string back_extra = 4; repeated string tags = 5; string image_file_name = 6; - bool occlude_inactive = 7; + OcclusionMode occlusion_mode = 7; } oneof value { diff --git a/rslib/src/image_occlusion/imagedata.rs b/rslib/src/image_occlusion/imagedata.rs index fdf8ea4fd..b8609ccdd 100644 --- a/rslib/src/image_occlusion/imagedata.rs +++ b/rslib/src/image_occlusion/imagedata.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use anki_io::metadata; use anki_io::read_file; use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionNote; +use anki_proto::image_occlusion::get_image_occlusion_note_response::OcclusionMode; use anki_proto::image_occlusion::get_image_occlusion_note_response::Value; use anki_proto::image_occlusion::AddImageOcclusionNoteRequest; use anki_proto::image_occlusion::GetImageForOcclusionResponse; @@ -97,14 +98,22 @@ impl Collection { let idxs = nt.get_io_field_indexes()?; cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str()); - cloze_note.occlude_inactive = cloze_note.occlusions.iter().any(|oc| { - oc.shapes.iter().any(|sh| { - sh.properties - .iter() - .find(|p| p.name == "oi") - .is_some_and(|p| p.value == "1") + cloze_note.occlusion_mode = cloze_note + .occlusions + .iter() + .find_map(|oc| { + oc.shapes.iter().find_map(|sh| { + sh.properties + .iter() + .find(|p| p.name == "oi") + .and_then(|p| match p.value.as_str() { + "1" => Some(OcclusionMode::HideAll as i32), + "2" => Some(OcclusionMode::HideAllButOne as i32), + _ => None, + }) + }) }) - }); + .unwrap_or(OcclusionMode::HideOne as i32); cloze_note.header.clone_from(&fields[idxs.header as usize]); cloze_note .back_extra diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 17ced575b..8bce99512 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -423,9 +423,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { IOMode } from "../routes/image-occlusion/lib"; import { exportShapesToClozeDeletions } from "../routes/image-occlusion/shapes/to-cloze"; import { - hideAllGuessOne, ioImageLoadedStore, ioMaskEditorVisible, + occlusionMode, } from "../routes/image-occlusion/store"; import CollapseLabel from "./CollapseLabel.svelte"; import * as oldEditorAdapter from "./old-editor-adapter"; @@ -477,7 +477,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html function saveOcclusions(): void { if (isImageOcclusion && globalThis.canvas) { - const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne); + const occlusionsData = exportShapesToClozeDeletions($occlusionMode); fieldStores[ioFields.occlusions].set(occlusionsData.clozes); } } diff --git a/ts/lib/components/icons.ts b/ts/lib/components/icons.ts index ab07cbf17..f3dbd64c8 100644 --- a/ts/lib/components/icons.ts +++ b/ts/lib/components/icons.ts @@ -15,6 +15,8 @@ import AlignVerticalCenter_ from "@mdi/svg/svg/align-vertical-center.svg?compone import alignVerticalCenter_ from "@mdi/svg/svg/align-vertical-center.svg?url"; import AlignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?component"; import alignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?url"; +import CheckboxBlankOutline_ from "@mdi/svg/svg/checkbox-blank-outline.svg?component"; +import checkboxBlankOutline_ from "@mdi/svg/svg/checkbox-blank-outline.svg?url"; import CheckCircle_ from "@mdi/svg/svg/check-circle.svg?component"; import checkCircle_ from "@mdi/svg/svg/check-circle.svg?url"; import ChevronDown_ from "@mdi/svg/svg/chevron-down.svg?component"; @@ -251,6 +253,7 @@ export const underlineIcon = { url: underline_, component: Underline_ }; export const deleteIcon = { url: delete_, component: Delete_ }; export const inlineIcon = { url: inline_, component: Inline_ }; export const blockIcon = { url: block_, component: Block_ }; +export const mdiCheckboxBlankOutline = { url: checkboxBlankOutline_, component: CheckboxBlankOutline_ }; export const mdiAlignHorizontalCenter = { url: alignHorizontalCenter_, component: AlignHorizontalCenter_ }; export const mdiAlignHorizontalLeft = { url: alignHorizontalLeft_, component: AlignHorizontalLeft_ }; export const mdiAlignHorizontalRight = { url: alignHorizontalRight_, component: AlignHorizontalRight_ }; diff --git a/ts/routes/image-occlusion/Toolbar.svelte b/ts/routes/image-occlusion/Toolbar.svelte index b00e42087..354ae89f4 100644 --- a/ts/routes/image-occlusion/Toolbar.svelte +++ b/ts/routes/image-occlusion/Toolbar.svelte @@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Icon from "$lib/components/Icon.svelte"; import IconButton from "$lib/components/IconButton.svelte"; import { + mdiCheckboxBlankOutline, mdiEye, mdiFormatAlignCenter, mdiSquare, @@ -26,11 +27,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import WithFloating from "$lib/components/WithFloating.svelte"; import { - hideAllGuessOne, ioMaskEditorVisible, - textEditingState, - saveNeededStore, + OcclusionMode, + occlusionMode, opacityStateStore, + saveNeededStore, + textEditingState, } from "./store"; import { get } from "svelte/store"; import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index"; @@ -228,8 +230,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html disablePan(canvas); }; - function changeOcclusionType(occlusionType: "all" | "one"): void { - $hideAllGuessOne = occlusionType === "all"; + function changeOcclusionType(mode: OcclusionMode): void { + $occlusionMode = mode; saveNeededStore.set(true); } @@ -312,22 +314,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {iconSize} on:click={() => (showFloating = !showFloating)} > - + changeOcclusionType("all")} + active={$occlusionMode === OcclusionMode.HideAll} + on:click={() => changeOcclusionType(OcclusionMode.HideAll)} > {tr.notetypesHideAllGuessOne()} changeOcclusionType("one")} + active={$occlusionMode === OcclusionMode.HideOne} + on:click={() => changeOcclusionType(OcclusionMode.HideOne)} > {tr.notetypesHideOneGuessOne()} + changeOcclusionType(OcclusionMode.HideAllButOne)} + > + {tr.notetypesHideAllButOne()} + diff --git a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts index 6f83cc44c..6de4cd347 100644 --- a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts +++ b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts @@ -5,11 +5,11 @@ import { get } from "svelte/store"; import { addOrUpdateNote } from "../add-or-update-note.svelte"; import type { IOMode } from "../lib"; -import { hideAllGuessOne } from "../store"; +import { occlusionMode } from "../store"; import type { PageLoad } from "./$types"; async function save(): Promise { - addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(hideAllGuessOne)); + addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(occlusionMode)); } export const load = (async ({ params }) => { diff --git a/ts/routes/image-occlusion/add-or-update-note.svelte.ts b/ts/routes/image-occlusion/add-or-update-note.svelte.ts index ce31eaaaf..c8af2d970 100644 --- a/ts/routes/image-occlusion/add-or-update-note.svelte.ts +++ b/ts/routes/image-occlusion/add-or-update-note.svelte.ts @@ -9,14 +9,14 @@ import { get } from "svelte/store"; import { mount } from "svelte"; import type { IOAddingMode, IOMode } from "./lib"; import { exportShapesToClozeDeletions } from "./shapes/to-cloze"; -import { notesDataStore, tagsWritable } from "./store"; +import { notesDataStore, OcclusionMode, tagsWritable } from "./store"; import Toast from "./Toast.svelte"; export const addOrUpdateNote = async function( mode: IOMode, - occludeInactive: boolean, + occlusionMode: OcclusionMode, ): Promise { - const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occludeInactive); + const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occlusionMode); if (noteCount === 0) { return; } diff --git a/ts/routes/image-occlusion/index.ts b/ts/routes/image-occlusion/index.ts index 59720b1e0..942babb13 100644 --- a/ts/routes/image-occlusion/index.ts +++ b/ts/routes/image-occlusion/index.ts @@ -10,7 +10,7 @@ import { get } from "svelte/store"; import { addOrUpdateNote } from "./add-or-update-note.svelte"; import ImageOcclusionPage from "./ImageOcclusionPage.svelte"; import type { IOMode } from "./lib"; -import { hideAllGuessOne } from "./store"; +import { occlusionMode } from "./store"; globalThis.anki = globalThis.anki || {}; @@ -31,7 +31,7 @@ export async function setupImageOcclusion(mode: IOMode, target = document.body): await i18n; async function addNote(): Promise { - addOrUpdateNote(mode, get(hideAllGuessOne)); + addOrUpdateNote(mode, get(occlusionMode)); } // for adding note from mobile devices diff --git a/ts/routes/image-occlusion/mask-editor.ts b/ts/routes/image-occlusion/mask-editor.ts index 41adbe423..aca9f69ee 100644 --- a/ts/routes/image-occlusion/mask-editor.ts +++ b/ts/routes/image-occlusion/mask-editor.ts @@ -9,8 +9,8 @@ import { get } from "svelte/store"; import { optimumCssSizeForCanvas } from "./canvas-scale"; import { - hideAllGuessOne, notesDataStore, + occlusionMode, opacityStateStore, saveNeededStore, tagsWritable, @@ -75,7 +75,7 @@ export const setupMaskEditorForEdit = async ( const clozeNote = clozeNoteResponse.value.value; const canvas = initCanvas(); - hideAllGuessOne.set(clozeNote.occludeInactive); + occlusionMode.set(clozeNote.occlusionMode); // get image width and height const image = document.getElementById("image") as HTMLImageElement; diff --git a/ts/routes/image-occlusion/review.ts b/ts/routes/image-occlusion/review.ts index 4425a8141..22e81b432 100644 --- a/ts/routes/image-occlusion/review.ts +++ b/ts/routes/image-occlusion/review.ts @@ -168,7 +168,7 @@ async function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOption // setup button for toggle image occlusion const button = document.getElementById("toggle"); if (button) { - if (document.querySelector("[data-occludeinactive=\"1\"]")) { + if (document.querySelector("[data-occludeinactive=\"1\"], [data-occludeinactive=\"2\"]")) { button.addEventListener("click", () => toggleMasks(setupOptions)); } else { button.style.display = "none"; @@ -202,35 +202,55 @@ function drawShapes( properties = processed.properties; } - for (const shape of activeShapes) { - drawShape({ - context, - size, - shape, - fill: properties.activeShapeColor, - stroke: properties.activeBorder.color, - strokeWidth: properties.activeBorder.width, - }); - } - for (const shape of inactiveShapes.filter((s) => s.occludeInactive)) { - drawShape({ - context, - size, - shape, - fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor, - stroke: properties.inActiveBorder.color, - strokeWidth: properties.inActiveBorder.width, - }); - } - for (const shape of highlightShapes) { - drawShape({ - context, - size, - shape, - fill: properties.highlightShapeColor, - stroke: properties.highlightShapeBorder.color, - strokeWidth: properties.highlightShapeBorder.width, - }); + // Determine occlusion mode from the first shape + const occlusionMode = activeShapes[0]?.occlusionMode ?? inactiveShapes[0]?.occlusionMode ?? 0; + + // Mode 0 (HideOne): Draw active only (front), reveal answer with highlight (back) + // Mode 1 (HideAll): Draw both active and inactive (front & back) + // Mode 2 (HideAllButOne): Draw inactive only (front), draw nothing (back) + + // Check if we're on the back side (highlightShapes only exist on back) + const isBackSide = highlightShapes.length > 0; + + // For mode 2 on the back side, draw nothing (show full unoccluded image) + if (occlusionMode === 2 && isBackSide) { + // Don't draw any shapes on the back for "Hide All But One" mode + } else { + // Normal drawing logic for all other cases + if (occlusionMode !== 2) { + for (const shape of activeShapes) { + drawShape({ + context, + size, + shape, + fill: properties.activeShapeColor, + stroke: properties.activeBorder.color, + strokeWidth: properties.activeBorder.width, + }); + } + } + if (occlusionMode === 1 || occlusionMode === 2) { + for (const shape of inactiveShapes) { + drawShape({ + context, + size, + shape, + fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor, + stroke: properties.inActiveBorder.color, + strokeWidth: properties.inActiveBorder.width, + }); + } + } + for (const shape of highlightShapes) { + drawShape({ + context, + size, + shape, + fill: properties.highlightShapeColor, + stroke: properties.highlightShapeBorder.color, + strokeWidth: properties.highlightShapeBorder.width, + }); + } } onDidDrawShapes?.({ diff --git a/ts/routes/image-occlusion/shapes/base.ts b/ts/routes/image-occlusion/shapes/base.ts index 6dd65f122..bb3da0e73 100644 --- a/ts/routes/image-occlusion/shapes/base.ts +++ b/ts/routes/image-occlusion/shapes/base.ts @@ -20,23 +20,23 @@ export class Shape { top: number; angle?: number; // polygons don't use it fill: string; - /** Whether occlusions from other cloze numbers should be shown on the - * question side. Used only in reviewer code. + /** Occlusion mode: 0=HideOne, 1=HideAll, 2=HideAllButOne. + * Used only in reviewer code. */ - occludeInactive?: boolean; + occlusionMode?: number; /* Cloze ordinal */ ordinal: number | undefined; id: string | undefined; constructor( - { left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }: + { left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occlusionMode, ordinal = undefined }: ConstructorParams = {}, ) { this.left = left; this.top = top; this.angle = angle; this.fill = fill; - this.occludeInactive = occludeInactive; + this.occlusionMode = occlusionMode; this.ordinal = ordinal; } diff --git a/ts/routes/image-occlusion/shapes/from-cloze.ts b/ts/routes/image-occlusion/shapes/from-cloze.ts index 0db496740..aeb8f1487 100644 --- a/ts/routes/image-occlusion/shapes/from-cloze.ts +++ b/ts/routes/image-occlusion/shapes/from-cloze.ts @@ -64,7 +64,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null { return null; } const props = { - occludeInactive: cloze.dataset.occludeinactive === "1", + occlusionMode: cloze.dataset.occludeinactive ? parseInt(cloze.dataset.occludeinactive) : undefined, ordinal: parseInt(cloze.dataset.ordinal!), left: cloze.dataset.left, top: cloze.dataset.top, diff --git a/ts/routes/image-occlusion/shapes/to-cloze.ts b/ts/routes/image-occlusion/shapes/to-cloze.ts index 102e9bbd4..6c1027f6f 100644 --- a/ts/routes/image-occlusion/shapes/to-cloze.ts +++ b/ts/routes/image-occlusion/shapes/to-cloze.ts @@ -4,6 +4,7 @@ import { fabric } from "fabric"; import { cloneDeep } from "lodash-es"; +import { OcclusionMode } from "../store"; import { getBoundingBoxSize } from "../tools/lib"; import type { Size } from "../types"; import type { Shape, ShapeOrShapes } from "./base"; @@ -12,7 +13,7 @@ import { Polygon } from "./polygon"; import { Rectangle } from "./rectangle"; import { Text } from "./text"; -export function exportShapesToClozeDeletions(occludeInactive: boolean): { +export function exportShapesToClozeDeletions(mode: OcclusionMode): { clozes: string; noteCount: number; } { @@ -76,7 +77,7 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): { clozes += shapeOrShapesToCloze( shapeOrShapes, ordinal, - occludeInactive, + mode, ); if (!(shapeOrShapes instanceof Text)) { @@ -179,7 +180,7 @@ function fabricObjectToBaseShapeOrShapes( function shapeOrShapesToCloze( shapeOrShapes: ShapeOrShapes, ordinal: number, - occludeInactive: boolean, + mode: OcclusionMode, ): string { let text = ""; function addKeyValue(key: string, value: string) { @@ -190,7 +191,7 @@ function shapeOrShapesToCloze( let type: string; if (Array.isArray(shapeOrShapes)) { return shapeOrShapes - .map((shape) => shapeOrShapesToCloze(shape, ordinal, occludeInactive)) + .map((shape) => shapeOrShapesToCloze(shape, ordinal, mode)) .join(""); } else if (shapeOrShapes instanceof Rectangle) { type = "rect"; @@ -207,8 +208,8 @@ function shapeOrShapesToCloze( for (const [key, value] of Object.entries(shapeOrShapes.toDataForCloze())) { addKeyValue(key, value); } - if (occludeInactive) { - addKeyValue("oi", "1"); + if (mode !== OcclusionMode.HideOne) { + addKeyValue("oi", mode.toString()); } text = `{{c${ordinal}::image-occlusion:${type}${text}}}
`; diff --git a/ts/routes/image-occlusion/store.ts b/ts/routes/image-occlusion/store.ts index 0af7696eb..97b1d9a22 100644 --- a/ts/routes/image-occlusion/store.ts +++ b/ts/routes/image-occlusion/store.ts @@ -3,14 +3,20 @@ import { writable } from "svelte/store"; +export enum OcclusionMode { + HideOne = 0, + HideAll = 1, + HideAllButOne = 2, +} + // it stores note's data for generate.ts, when function generate() is called it will be used to generate the note export const notesDataStore = writable({ id: "", title: "", divValue: "", textareaValue: "" }[0]); // it stores the tags for the note in note editor export const tagsWritable = writable([""]); // it stores the visibility of mask editor export const ioMaskEditorVisible = writable(true); -// it store hide all or hide one mode -export const hideAllGuessOne = writable(true); +// it stores the occlusion mode (hide one, hide all, or hide all reveal one) +export const occlusionMode = writable(OcclusionMode.HideAll); // ioImageLoadedStore is used to store the image loaded event export const ioImageLoadedStore = writable(false); // store opacity state of objects in canvas From fa9d6840257563ef7c3ec6c92275a12ea17bacae Mon Sep 17 00:00:00 2001 From: jariji Date: Tue, 4 Nov 2025 00:46:49 +0000 Subject: [PATCH 2/3] use enum --- ts/routes/image-occlusion/review.ts | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/ts/routes/image-occlusion/review.ts b/ts/routes/image-occlusion/review.ts index 22e81b432..fa627f49d 100644 --- a/ts/routes/image-occlusion/review.ts +++ b/ts/routes/image-occlusion/review.ts @@ -7,6 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n"; import { optimumPixelSizeForCanvas } from "./canvas-scale"; import { Shape } from "./shapes"; import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } from "./shapes"; +import { OcclusionMode } from "./store"; import { SHAPE_MASK_COLOR, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib"; import type { Size } from "./types"; @@ -168,10 +169,18 @@ async function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOption // setup button for toggle image occlusion const button = document.getElementById("toggle"); if (button) { - if (document.querySelector("[data-occludeinactive=\"1\"], [data-occludeinactive=\"2\"]")) { - button.addEventListener("click", () => toggleMasks(setupOptions)); - } else { + const hasHideAllMode = document.querySelector(`[data-occludeinactive="${OcclusionMode.HideAll}"]`); + const hasHideAllButOneMode = document.querySelector(`[data-occludeinactive="${OcclusionMode.HideAllButOne}"]`); + const hasAnyToggleableMode = hasHideAllMode || hasHideAllButOneMode; + const isBackSide = document.querySelectorAll(".cloze-highlight").length > 0; + + // Hide button if: + // 1. No shapes with Hide All or Hide All But One modes, OR + // 2. Hide All But One mode on the back side (no shapes to toggle) + if (!hasAnyToggleableMode || (hasHideAllButOneMode && isBackSide)) { button.style.display = "none"; + } else { + button.addEventListener("click", () => toggleMasks(setupOptions)); } } @@ -203,21 +212,21 @@ function drawShapes( } // Determine occlusion mode from the first shape - const occlusionMode = activeShapes[0]?.occlusionMode ?? inactiveShapes[0]?.occlusionMode ?? 0; + const occlusionMode = activeShapes[0]?.occlusionMode ?? inactiveShapes[0]?.occlusionMode ?? OcclusionMode.HideOne; - // Mode 0 (HideOne): Draw active only (front), reveal answer with highlight (back) - // Mode 1 (HideAll): Draw both active and inactive (front & back) - // Mode 2 (HideAllButOne): Draw inactive only (front), draw nothing (back) + // HideOne: Draw active only (front), reveal answer with highlight (back) + // HideAll: Draw both active and inactive (front & back) + // HideAllButOne: Draw inactive only (front), draw nothing (back) // Check if we're on the back side (highlightShapes only exist on back) const isBackSide = highlightShapes.length > 0; - // For mode 2 on the back side, draw nothing (show full unoccluded image) - if (occlusionMode === 2 && isBackSide) { + // For HideAllButOne on the back side, draw nothing (show full unoccluded image) + if (occlusionMode === OcclusionMode.HideAllButOne && isBackSide) { // Don't draw any shapes on the back for "Hide All But One" mode } else { // Normal drawing logic for all other cases - if (occlusionMode !== 2) { + if (occlusionMode !== OcclusionMode.HideAllButOne) { for (const shape of activeShapes) { drawShape({ context, @@ -229,7 +238,7 @@ function drawShapes( }); } } - if (occlusionMode === 1 || occlusionMode === 2) { + if (occlusionMode === OcclusionMode.HideAll || occlusionMode === OcclusionMode.HideAllButOne) { for (const shape of inactiveShapes) { drawShape({ context, From 46e24d7f3f7ba3c52888f7e85643763b9449aff3 Mon Sep 17 00:00:00 2001 From: jariji Date: Tue, 4 Nov 2025 00:50:38 +0000 Subject: [PATCH 3/3] test --- rslib/src/cloze.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index 70a5d1703..9b0987312 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -839,4 +839,22 @@ mod test { let card2_html = reveal_cloze_text(text, 2, true); assert!(card2_html.contains(r#"data-ordinal="1,2""#)); } + + #[test] + fn image_occlusion_modes() { + // Mode 1 (HideAll): should include data-occludeinactive="1" + let hide_all = "{{c1::image-occlusion:rect:left=10:top=20:width=30:height=40:oi=1}}"; + let html = reveal_cloze_text(hide_all, 1, true); + assert!(html.contains(r#"data-occludeInactive="1""#)); + + // Mode 2 (HideAllButOne): should include data-occludeinactive="2" + let hide_all_but_one = "{{c1::image-occlusion:rect:left=10:top=20:width=30:height=40:oi=2}}"; + let html = reveal_cloze_text(hide_all_but_one, 1, true); + assert!(html.contains(r#"data-occludeInactive="2""#)); + + // Mode 0 (HideOne): should not include data-occludeinactive attribute + let hide_one = "{{c1::image-occlusion:rect:left=10:top=20:width=30:height=40}}"; + let html = reveal_cloze_text(hide_one, 1, true); + assert!(!html.contains("data-occludeInactive")); + } }