mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 12:47:11 -05:00
Merge 46e24d7f3f into dac26ce671
This commit is contained in:
commit
d92966ddb4
16 changed files with 162 additions and 75 deletions
|
|
@ -47,6 +47,7 @@ notetypes-toggle-masks = Toggle Masks
|
||||||
notetypes-image-occlusion-name = Image Occlusion
|
notetypes-image-occlusion-name = Image Occlusion
|
||||||
notetypes-hide-all-guess-one = Hide All, Guess One
|
notetypes-hide-all-guess-one = Hide All, Guess One
|
||||||
notetypes-hide-one-guess-one = Hide One, 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-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-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?
|
notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date?
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,12 @@ message GetImageOcclusionNoteResponse {
|
||||||
uint32 ordinal = 2;
|
uint32 ordinal = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OcclusionMode {
|
||||||
|
HIDE_ONE = 0;
|
||||||
|
HIDE_ALL = 1;
|
||||||
|
HIDE_ALL_BUT_ONE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ImageOcclusionNote {
|
message ImageOcclusionNote {
|
||||||
bytes image_data = 1;
|
bytes image_data = 1;
|
||||||
repeated ImageOcclusion occlusions = 2;
|
repeated ImageOcclusion occlusions = 2;
|
||||||
|
|
@ -76,7 +82,7 @@ message GetImageOcclusionNoteResponse {
|
||||||
string back_extra = 4;
|
string back_extra = 4;
|
||||||
repeated string tags = 5;
|
repeated string tags = 5;
|
||||||
string image_file_name = 6;
|
string image_file_name = 6;
|
||||||
bool occlude_inactive = 7;
|
OcclusionMode occlusion_mode = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
oneof value {
|
oneof value {
|
||||||
|
|
|
||||||
|
|
@ -839,4 +839,22 @@ mod test {
|
||||||
let card2_html = reveal_cloze_text(text, 2, true);
|
let card2_html = reveal_cloze_text(text, 2, true);
|
||||||
assert!(card2_html.contains(r#"data-ordinal="1,2""#));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||||
use anki_io::metadata;
|
use anki_io::metadata;
|
||||||
use anki_io::read_file;
|
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::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::get_image_occlusion_note_response::Value;
|
||||||
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
|
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
|
||||||
use anki_proto::image_occlusion::GetImageForOcclusionResponse;
|
use anki_proto::image_occlusion::GetImageForOcclusionResponse;
|
||||||
|
|
@ -97,14 +98,22 @@ impl Collection {
|
||||||
let idxs = nt.get_io_field_indexes()?;
|
let idxs = nt.get_io_field_indexes()?;
|
||||||
|
|
||||||
cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str());
|
cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str());
|
||||||
cloze_note.occlude_inactive = cloze_note.occlusions.iter().any(|oc| {
|
cloze_note.occlusion_mode = cloze_note
|
||||||
oc.shapes.iter().any(|sh| {
|
.occlusions
|
||||||
sh.properties
|
.iter()
|
||||||
.iter()
|
.find_map(|oc| {
|
||||||
.find(|p| p.name == "oi")
|
oc.shapes.iter().find_map(|sh| {
|
||||||
.is_some_and(|p| p.value == "1")
|
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.header.clone_from(&fields[idxs.header as usize]);
|
||||||
cloze_note
|
cloze_note
|
||||||
.back_extra
|
.back_extra
|
||||||
|
|
|
||||||
|
|
@ -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 type { IOMode } from "../routes/image-occlusion/lib";
|
||||||
import { exportShapesToClozeDeletions } from "../routes/image-occlusion/shapes/to-cloze";
|
import { exportShapesToClozeDeletions } from "../routes/image-occlusion/shapes/to-cloze";
|
||||||
import {
|
import {
|
||||||
hideAllGuessOne,
|
|
||||||
ioImageLoadedStore,
|
ioImageLoadedStore,
|
||||||
ioMaskEditorVisible,
|
ioMaskEditorVisible,
|
||||||
|
occlusionMode,
|
||||||
} from "../routes/image-occlusion/store";
|
} from "../routes/image-occlusion/store";
|
||||||
import CollapseLabel from "./CollapseLabel.svelte";
|
import CollapseLabel from "./CollapseLabel.svelte";
|
||||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
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 {
|
function saveOcclusions(): void {
|
||||||
if (isImageOcclusion && globalThis.canvas) {
|
if (isImageOcclusion && globalThis.canvas) {
|
||||||
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
|
const occlusionsData = exportShapesToClozeDeletions($occlusionMode);
|
||||||
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
|
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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?component";
|
||||||
import alignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?url";
|
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?component";
|
||||||
import checkCircle_ from "@mdi/svg/svg/check-circle.svg?url";
|
import checkCircle_ from "@mdi/svg/svg/check-circle.svg?url";
|
||||||
import ChevronDown_ from "@mdi/svg/svg/chevron-down.svg?component";
|
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 deleteIcon = { url: delete_, component: Delete_ };
|
||||||
export const inlineIcon = { url: inline_, component: Inline_ };
|
export const inlineIcon = { url: inline_, component: Inline_ };
|
||||||
export const blockIcon = { url: block_, component: Block_ };
|
export const blockIcon = { url: block_, component: Block_ };
|
||||||
|
export const mdiCheckboxBlankOutline = { url: checkboxBlankOutline_, component: CheckboxBlankOutline_ };
|
||||||
export const mdiAlignHorizontalCenter = { url: alignHorizontalCenter_, component: AlignHorizontalCenter_ };
|
export const mdiAlignHorizontalCenter = { url: alignHorizontalCenter_, component: AlignHorizontalCenter_ };
|
||||||
export const mdiAlignHorizontalLeft = { url: alignHorizontalLeft_, component: AlignHorizontalLeft_ };
|
export const mdiAlignHorizontalLeft = { url: alignHorizontalLeft_, component: AlignHorizontalLeft_ };
|
||||||
export const mdiAlignHorizontalRight = { url: alignHorizontalRight_, component: AlignHorizontalRight_ };
|
export const mdiAlignHorizontalRight = { url: alignHorizontalRight_, component: AlignHorizontalRight_ };
|
||||||
|
|
|
||||||
|
|
@ -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 Icon from "$lib/components/Icon.svelte";
|
||||||
import IconButton from "$lib/components/IconButton.svelte";
|
import IconButton from "$lib/components/IconButton.svelte";
|
||||||
import {
|
import {
|
||||||
|
mdiCheckboxBlankOutline,
|
||||||
mdiEye,
|
mdiEye,
|
||||||
mdiFormatAlignCenter,
|
mdiFormatAlignCenter,
|
||||||
mdiSquare,
|
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 WithFloating from "$lib/components/WithFloating.svelte";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hideAllGuessOne,
|
|
||||||
ioMaskEditorVisible,
|
ioMaskEditorVisible,
|
||||||
textEditingState,
|
OcclusionMode,
|
||||||
saveNeededStore,
|
occlusionMode,
|
||||||
opacityStateStore,
|
opacityStateStore,
|
||||||
|
saveNeededStore,
|
||||||
|
textEditingState,
|
||||||
} from "./store";
|
} from "./store";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
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);
|
disablePan(canvas);
|
||||||
};
|
};
|
||||||
|
|
||||||
function changeOcclusionType(occlusionType: "all" | "one"): void {
|
function changeOcclusionType(mode: OcclusionMode): void {
|
||||||
$hideAllGuessOne = occlusionType === "all";
|
$occlusionMode = mode;
|
||||||
saveNeededStore.set(true);
|
saveNeededStore.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,22 +314,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{iconSize}
|
{iconSize}
|
||||||
on:click={() => (showFloating = !showFloating)}
|
on:click={() => (showFloating = !showFloating)}
|
||||||
>
|
>
|
||||||
<Icon icon={$hideAllGuessOne ? mdiViewDashboard : mdiSquare} />
|
<Icon
|
||||||
|
icon={$occlusionMode === OcclusionMode.HideAll
|
||||||
|
? mdiViewDashboard
|
||||||
|
: $occlusionMode === OcclusionMode.HideAllButOne
|
||||||
|
? mdiCheckboxBlankOutline
|
||||||
|
: mdiSquare}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Popover slot="floating">
|
<Popover slot="floating">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
active={$hideAllGuessOne}
|
active={$occlusionMode === OcclusionMode.HideAll}
|
||||||
on:click={() => changeOcclusionType("all")}
|
on:click={() => changeOcclusionType(OcclusionMode.HideAll)}
|
||||||
>
|
>
|
||||||
<span>{tr.notetypesHideAllGuessOne()}</span>
|
<span>{tr.notetypesHideAllGuessOne()}</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
active={!$hideAllGuessOne}
|
active={$occlusionMode === OcclusionMode.HideOne}
|
||||||
on:click={() => changeOcclusionType("one")}
|
on:click={() => changeOcclusionType(OcclusionMode.HideOne)}
|
||||||
>
|
>
|
||||||
<span>{tr.notetypesHideOneGuessOne()}</span>
|
<span>{tr.notetypesHideOneGuessOne()}</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
active={$occlusionMode === OcclusionMode.HideAllButOne}
|
||||||
|
on:click={() => changeOcclusionType(OcclusionMode.HideAllButOne)}
|
||||||
|
>
|
||||||
|
<span>{tr.notetypesHideAllButOne()}</span>
|
||||||
|
</DropdownItem>
|
||||||
</Popover>
|
</Popover>
|
||||||
</WithFloating>
|
</WithFloating>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import { get } from "svelte/store";
|
||||||
|
|
||||||
import { addOrUpdateNote } from "../add-or-update-note.svelte";
|
import { addOrUpdateNote } from "../add-or-update-note.svelte";
|
||||||
import type { IOMode } from "../lib";
|
import type { IOMode } from "../lib";
|
||||||
import { hideAllGuessOne } from "../store";
|
import { occlusionMode } from "../store";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
async function save(): Promise<void> {
|
||||||
addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(hideAllGuessOne));
|
addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(occlusionMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
export const load = (async ({ params }) => {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ import { get } from "svelte/store";
|
||||||
import { mount } from "svelte";
|
import { mount } from "svelte";
|
||||||
import type { IOAddingMode, IOMode } from "./lib";
|
import type { IOAddingMode, IOMode } from "./lib";
|
||||||
import { exportShapesToClozeDeletions } from "./shapes/to-cloze";
|
import { exportShapesToClozeDeletions } from "./shapes/to-cloze";
|
||||||
import { notesDataStore, tagsWritable } from "./store";
|
import { notesDataStore, OcclusionMode, tagsWritable } from "./store";
|
||||||
import Toast from "./Toast.svelte";
|
import Toast from "./Toast.svelte";
|
||||||
|
|
||||||
export const addOrUpdateNote = async function(
|
export const addOrUpdateNote = async function(
|
||||||
mode: IOMode,
|
mode: IOMode,
|
||||||
occludeInactive: boolean,
|
occlusionMode: OcclusionMode,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occludeInactive);
|
const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occlusionMode);
|
||||||
if (noteCount === 0) {
|
if (noteCount === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { get } from "svelte/store";
|
||||||
import { addOrUpdateNote } from "./add-or-update-note.svelte";
|
import { addOrUpdateNote } from "./add-or-update-note.svelte";
|
||||||
import ImageOcclusionPage from "./ImageOcclusionPage.svelte";
|
import ImageOcclusionPage from "./ImageOcclusionPage.svelte";
|
||||||
import type { IOMode } from "./lib";
|
import type { IOMode } from "./lib";
|
||||||
import { hideAllGuessOne } from "./store";
|
import { occlusionMode } from "./store";
|
||||||
|
|
||||||
globalThis.anki = globalThis.anki || {};
|
globalThis.anki = globalThis.anki || {};
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ export async function setupImageOcclusion(mode: IOMode, target = document.body):
|
||||||
await i18n;
|
await i18n;
|
||||||
|
|
||||||
async function addNote(): Promise<void> {
|
async function addNote(): Promise<void> {
|
||||||
addOrUpdateNote(mode, get(hideAllGuessOne));
|
addOrUpdateNote(mode, get(occlusionMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
// for adding note from mobile devices
|
// for adding note from mobile devices
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { get } from "svelte/store";
|
||||||
|
|
||||||
import { optimumCssSizeForCanvas } from "./canvas-scale";
|
import { optimumCssSizeForCanvas } from "./canvas-scale";
|
||||||
import {
|
import {
|
||||||
hideAllGuessOne,
|
|
||||||
notesDataStore,
|
notesDataStore,
|
||||||
|
occlusionMode,
|
||||||
opacityStateStore,
|
opacityStateStore,
|
||||||
saveNeededStore,
|
saveNeededStore,
|
||||||
tagsWritable,
|
tagsWritable,
|
||||||
|
|
@ -75,7 +75,7 @@ export const setupMaskEditorForEdit = async (
|
||||||
const clozeNote = clozeNoteResponse.value.value;
|
const clozeNote = clozeNoteResponse.value.value;
|
||||||
const canvas = initCanvas();
|
const canvas = initCanvas();
|
||||||
|
|
||||||
hideAllGuessOne.set(clozeNote.occludeInactive);
|
occlusionMode.set(clozeNote.occlusionMode);
|
||||||
|
|
||||||
// get image width and height
|
// get image width and height
|
||||||
const image = document.getElementById("image") as HTMLImageElement;
|
const image = document.getElementById("image") as HTMLImageElement;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||||
import { optimumPixelSizeForCanvas } from "./canvas-scale";
|
import { optimumPixelSizeForCanvas } from "./canvas-scale";
|
||||||
import { Shape } from "./shapes";
|
import { Shape } from "./shapes";
|
||||||
import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } 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 { SHAPE_MASK_COLOR, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib";
|
||||||
import type { Size } from "./types";
|
import type { Size } from "./types";
|
||||||
|
|
||||||
|
|
@ -168,10 +169,18 @@ async function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOption
|
||||||
// setup button for toggle image occlusion
|
// setup button for toggle image occlusion
|
||||||
const button = document.getElementById("toggle");
|
const button = document.getElementById("toggle");
|
||||||
if (button) {
|
if (button) {
|
||||||
if (document.querySelector("[data-occludeinactive=\"1\"]")) {
|
const hasHideAllMode = document.querySelector(`[data-occludeinactive="${OcclusionMode.HideAll}"]`);
|
||||||
button.addEventListener("click", () => toggleMasks(setupOptions));
|
const hasHideAllButOneMode = document.querySelector(`[data-occludeinactive="${OcclusionMode.HideAllButOne}"]`);
|
||||||
} else {
|
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";
|
button.style.display = "none";
|
||||||
|
} else {
|
||||||
|
button.addEventListener("click", () => toggleMasks(setupOptions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,35 +211,55 @@ function drawShapes(
|
||||||
properties = processed.properties;
|
properties = processed.properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const shape of activeShapes) {
|
// Determine occlusion mode from the first shape
|
||||||
drawShape({
|
const occlusionMode = activeShapes[0]?.occlusionMode ?? inactiveShapes[0]?.occlusionMode ?? OcclusionMode.HideOne;
|
||||||
context,
|
|
||||||
size,
|
// HideOne: Draw active only (front), reveal answer with highlight (back)
|
||||||
shape,
|
// HideAll: Draw both active and inactive (front & back)
|
||||||
fill: properties.activeShapeColor,
|
// HideAllButOne: Draw inactive only (front), draw nothing (back)
|
||||||
stroke: properties.activeBorder.color,
|
|
||||||
strokeWidth: properties.activeBorder.width,
|
// Check if we're on the back side (highlightShapes only exist on back)
|
||||||
});
|
const isBackSide = highlightShapes.length > 0;
|
||||||
}
|
|
||||||
for (const shape of inactiveShapes.filter((s) => s.occludeInactive)) {
|
// For HideAllButOne on the back side, draw nothing (show full unoccluded image)
|
||||||
drawShape({
|
if (occlusionMode === OcclusionMode.HideAllButOne && isBackSide) {
|
||||||
context,
|
// Don't draw any shapes on the back for "Hide All But One" mode
|
||||||
size,
|
} else {
|
||||||
shape,
|
// Normal drawing logic for all other cases
|
||||||
fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor,
|
if (occlusionMode !== OcclusionMode.HideAllButOne) {
|
||||||
stroke: properties.inActiveBorder.color,
|
for (const shape of activeShapes) {
|
||||||
strokeWidth: properties.inActiveBorder.width,
|
drawShape({
|
||||||
});
|
context,
|
||||||
}
|
size,
|
||||||
for (const shape of highlightShapes) {
|
shape,
|
||||||
drawShape({
|
fill: properties.activeShapeColor,
|
||||||
context,
|
stroke: properties.activeBorder.color,
|
||||||
size,
|
strokeWidth: properties.activeBorder.width,
|
||||||
shape,
|
});
|
||||||
fill: properties.highlightShapeColor,
|
}
|
||||||
stroke: properties.highlightShapeBorder.color,
|
}
|
||||||
strokeWidth: properties.highlightShapeBorder.width,
|
if (occlusionMode === OcclusionMode.HideAll || occlusionMode === OcclusionMode.HideAllButOne) {
|
||||||
});
|
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?.({
|
onDidDrawShapes?.({
|
||||||
|
|
|
||||||
|
|
@ -20,23 +20,23 @@ export class Shape {
|
||||||
top: number;
|
top: number;
|
||||||
angle?: number; // polygons don't use it
|
angle?: number; // polygons don't use it
|
||||||
fill: string;
|
fill: string;
|
||||||
/** Whether occlusions from other cloze numbers should be shown on the
|
/** Occlusion mode: 0=HideOne, 1=HideAll, 2=HideAllButOne.
|
||||||
* question side. Used only in reviewer code.
|
* Used only in reviewer code.
|
||||||
*/
|
*/
|
||||||
occludeInactive?: boolean;
|
occlusionMode?: number;
|
||||||
/* Cloze ordinal */
|
/* Cloze ordinal */
|
||||||
ordinal: number | undefined;
|
ordinal: number | undefined;
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
|
|
||||||
constructor(
|
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<Shape> = {},
|
ConstructorParams<Shape> = {},
|
||||||
) {
|
) {
|
||||||
this.left = left;
|
this.left = left;
|
||||||
this.top = top;
|
this.top = top;
|
||||||
this.angle = angle;
|
this.angle = angle;
|
||||||
this.fill = fill;
|
this.fill = fill;
|
||||||
this.occludeInactive = occludeInactive;
|
this.occlusionMode = occlusionMode;
|
||||||
this.ordinal = ordinal;
|
this.ordinal = ordinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const props = {
|
const props = {
|
||||||
occludeInactive: cloze.dataset.occludeinactive === "1",
|
occlusionMode: cloze.dataset.occludeinactive ? parseInt(cloze.dataset.occludeinactive) : undefined,
|
||||||
ordinal: parseInt(cloze.dataset.ordinal!),
|
ordinal: parseInt(cloze.dataset.ordinal!),
|
||||||
left: cloze.dataset.left,
|
left: cloze.dataset.left,
|
||||||
top: cloze.dataset.top,
|
top: cloze.dataset.top,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
|
|
||||||
|
import { OcclusionMode } from "../store";
|
||||||
import { getBoundingBoxSize } from "../tools/lib";
|
import { getBoundingBoxSize } from "../tools/lib";
|
||||||
import type { Size } from "../types";
|
import type { Size } from "../types";
|
||||||
import type { Shape, ShapeOrShapes } from "./base";
|
import type { Shape, ShapeOrShapes } from "./base";
|
||||||
|
|
@ -12,7 +13,7 @@ import { Polygon } from "./polygon";
|
||||||
import { Rectangle } from "./rectangle";
|
import { Rectangle } from "./rectangle";
|
||||||
import { Text } from "./text";
|
import { Text } from "./text";
|
||||||
|
|
||||||
export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
export function exportShapesToClozeDeletions(mode: OcclusionMode): {
|
||||||
clozes: string;
|
clozes: string;
|
||||||
noteCount: number;
|
noteCount: number;
|
||||||
} {
|
} {
|
||||||
|
|
@ -76,7 +77,7 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
||||||
clozes += shapeOrShapesToCloze(
|
clozes += shapeOrShapesToCloze(
|
||||||
shapeOrShapes,
|
shapeOrShapes,
|
||||||
ordinal,
|
ordinal,
|
||||||
occludeInactive,
|
mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(shapeOrShapes instanceof Text)) {
|
if (!(shapeOrShapes instanceof Text)) {
|
||||||
|
|
@ -179,7 +180,7 @@ function fabricObjectToBaseShapeOrShapes(
|
||||||
function shapeOrShapesToCloze(
|
function shapeOrShapesToCloze(
|
||||||
shapeOrShapes: ShapeOrShapes,
|
shapeOrShapes: ShapeOrShapes,
|
||||||
ordinal: number,
|
ordinal: number,
|
||||||
occludeInactive: boolean,
|
mode: OcclusionMode,
|
||||||
): string {
|
): string {
|
||||||
let text = "";
|
let text = "";
|
||||||
function addKeyValue(key: string, value: string) {
|
function addKeyValue(key: string, value: string) {
|
||||||
|
|
@ -190,7 +191,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, ordinal, occludeInactive))
|
.map((shape) => shapeOrShapesToCloze(shape, ordinal, mode))
|
||||||
.join("");
|
.join("");
|
||||||
} else if (shapeOrShapes instanceof Rectangle) {
|
} else if (shapeOrShapes instanceof Rectangle) {
|
||||||
type = "rect";
|
type = "rect";
|
||||||
|
|
@ -207,8 +208,8 @@ function shapeOrShapesToCloze(
|
||||||
for (const [key, value] of Object.entries(shapeOrShapes.toDataForCloze())) {
|
for (const [key, value] of Object.entries(shapeOrShapes.toDataForCloze())) {
|
||||||
addKeyValue(key, value);
|
addKeyValue(key, value);
|
||||||
}
|
}
|
||||||
if (occludeInactive) {
|
if (mode !== OcclusionMode.HideOne) {
|
||||||
addKeyValue("oi", "1");
|
addKeyValue("oi", mode.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
|
text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,20 @@
|
||||||
|
|
||||||
import { writable } from "svelte/store";
|
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
|
// 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]);
|
export const notesDataStore = writable({ id: "", title: "", divValue: "", textareaValue: "" }[0]);
|
||||||
// it stores the tags for the note in note editor
|
// it stores the tags for the note in note editor
|
||||||
export const tagsWritable = writable([""]);
|
export const tagsWritable = writable([""]);
|
||||||
// it stores the visibility of mask editor
|
// it stores the visibility of mask editor
|
||||||
export const ioMaskEditorVisible = writable(true);
|
export const ioMaskEditorVisible = writable(true);
|
||||||
// it store hide all or hide one mode
|
// it stores the occlusion mode (hide one, hide all, or hide all reveal one)
|
||||||
export const hideAllGuessOne = writable(true);
|
export const occlusionMode = writable(OcclusionMode.HideAll);
|
||||||
// ioImageLoadedStore is used to store the image loaded event
|
// ioImageLoadedStore is used to store the image loaded event
|
||||||
export const ioImageLoadedStore = writable(false);
|
export const ioImageLoadedStore = writable(false);
|
||||||
// store opacity state of objects in canvas
|
// store opacity state of objects in canvas
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue