this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: include image rect clozes in review queue

build_review_queue now iterates sidecar rects (Vec<RectEntry>) in addition
to text blanks. Each due rect produces a ReviewItem with kind ImageRect {src, rect}.

Question render blacks out the rect region in the PNG by overwriting
pixels at the image box coordinates. Answer shows the card unmodified.
apply_review_schedule handles the rect-{id} sub_id by updating the
inline schedule on the matching RectEntry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+111 -11
+111 -11
crates/tala/src/main.rs
··· 1922 1922 enum ReviewKind { 1923 1923 FrontBack, 1924 1924 Cloze, 1925 + ImageRect { src: String, rect: [f32; 4] }, 1925 1926 } 1926 1927 1927 1928 #[derive(Clone)] ··· 1978 1979 } 1979 1980 } 1980 1981 (CardKind::Cloze { blanks, .. }, sched) => { 1981 - let blank_scheds: HashMap<String, Schedule> = match sched { 1982 - Some(CardSchedule::Cloze { blanks: b, .. }) => b.clone(), 1983 - _ => HashMap::new(), 1984 - }; 1982 + let (blank_scheds, sidecar_rects): (HashMap<String, Schedule>, Vec<_>) = 1983 + match sched { 1984 + Some(CardSchedule::Cloze { blanks: b, rects: r }) => { 1985 + (b.clone(), r.clone()) 1986 + } 1987 + _ => (HashMap::new(), Vec::new()), 1988 + }; 1985 1989 for blank in blanks { 1986 1990 let key = format!("b{}", blank.index); 1987 1991 let current = blank_scheds.get(&key).cloned(); ··· 1995 1999 }); 1996 2000 } 1997 2001 } 2002 + for rect_entry in sidecar_rects { 2003 + if is_due(&rect_entry.schedule) { 2004 + queue.push(ReviewItem { 2005 + source: card_src.clone(), 2006 + card_id: card_id.clone(), 2007 + sub_id: format!("rect-{}", rect_entry.id), 2008 + current: Some(rect_entry.schedule.clone()), 2009 + kind: ReviewKind::ImageRect { 2010 + src: rect_entry.src.clone(), 2011 + rect: rect_entry.rect, 2012 + }, 2013 + }); 2014 + } 2015 + } 1998 2016 } 1999 2017 } 2000 2018 } ··· 2040 2058 )) 2041 2059 } 2042 2060 2061 + fn render_rect_question_b64( 2062 + dir: &std::path::Path, 2063 + source: &str, 2064 + img_src: &str, 2065 + rect: [f32; 4], 2066 + ) -> Result<String, String> { 2067 + let result = tala_typst::render(dir, source, tala_typst::Preamble::Authoring, &[]) 2068 + .map_err(|e| e.to_string())?; 2069 + 2070 + let img_names = extract_img_names(source); 2071 + let img_idx = img_names.iter().position(|n| n == img_src); 2072 + 2073 + let mut straight: Vec<u8> = result 2074 + .rgba 2075 + .chunks_exact(4) 2076 + .flat_map(|px| { 2077 + let a = px[3]; 2078 + if a == 0 { 2079 + [0u8, 0, 0, 0] 2080 + } else { 2081 + let af = a as f32 / 255.0; 2082 + [ 2083 + (px[0] as f32 / af).min(255.0) as u8, 2084 + (px[1] as f32 / af).min(255.0) as u8, 2085 + (px[2] as f32 / af).min(255.0) as u8, 2086 + a, 2087 + ] 2088 + } 2089 + }) 2090 + .collect(); 2091 + 2092 + if let Some(idx) = img_idx { 2093 + if let Some(box_) = result.image_boxes.get(idx) { 2094 + let [bx, by, bw, bh] = *box_; // f32 pixel coords from RenderResult 2095 + let col0 = (bx + rect[0] * bw) as u32; 2096 + let row0 = (by + rect[1] * bh) as u32; 2097 + let col1 = ((bx + (rect[0] + rect[2]) * bw) as u32).min(result.width); 2098 + let row1 = ((by + (rect[1] + rect[3]) * bh) as u32).min(result.height); 2099 + let stride = result.width as usize * 4; 2100 + for row in row0.min(result.height)..row1 { 2101 + for col in col0.min(result.width)..col1 { 2102 + let off = row as usize * stride + col as usize * 4; 2103 + if off + 3 < straight.len() { 2104 + straight[off] = 30; 2105 + straight[off + 1] = 35; 2106 + straight[off + 2] = 50; 2107 + straight[off + 3] = 255; 2108 + } 2109 + } 2110 + } 2111 + } 2112 + } 2113 + 2114 + let img = RgbaImage::from_raw(result.width, result.height, straight) 2115 + .ok_or("image buffer size mismatch")?; 2116 + let mut buf = std::io::Cursor::new(Vec::new()); 2117 + image::DynamicImage::ImageRgba8(img) 2118 + .write_to(&mut buf, image::ImageFormat::Png) 2119 + .map_err(|e| e.to_string())?; 2120 + Ok(format!( 2121 + "data:image/png;base64,{}", 2122 + base64::engine::general_purpose::STANDARD.encode(buf.into_inner()) 2123 + )) 2124 + } 2125 + 2043 2126 fn apply_review_schedule( 2044 2127 sidecar: &mut Sidecar, 2045 2128 card_id: &str, ··· 2081 2164 blanks.insert(sub_id.to_owned(), sched); 2082 2165 } 2083 2166 } 2167 + ReviewKind::ImageRect { .. } => { 2168 + let rect_id = sub_id.strip_prefix("rect-").unwrap_or(sub_id); 2169 + if let Some(CardSchedule::Cloze { rects, .. }) = sidecar.cards.get_mut(card_id) { 2170 + if let Some(r) = rects.iter_mut().find(|r| r.id == rect_id) { 2171 + r.schedule = sched; 2172 + } 2173 + } 2174 + } 2084 2175 } 2085 2176 } 2086 2177 ··· 2161 2252 let Some(item) = item else { 2162 2253 return String::new(); 2163 2254 }; 2164 - let preamble = match (&item.kind, item.sub_id.as_str()) { 2165 - (ReviewKind::FrontBack, "reverse") => tala_typst::Preamble::ReviewBack, 2166 - (ReviewKind::FrontBack, _) => tala_typst::Preamble::ReviewFront, 2167 - (ReviewKind::Cloze, _) => tala_typst::Preamble::ReviewCloze, 2168 - }; 2169 2255 let dir = card_dir(); // capture in Dioxus runtime before spawn_blocking 2170 - tokio::task::spawn_blocking(move || { 2171 - render_review_b64(&dir, &item.source, preamble).unwrap_or_default() 2256 + tokio::task::spawn_blocking(move || match item.kind { 2257 + ReviewKind::FrontBack => { 2258 + let preamble = if item.sub_id == "reverse" { 2259 + tala_typst::Preamble::ReviewBack 2260 + } else { 2261 + tala_typst::Preamble::ReviewFront 2262 + }; 2263 + render_review_b64(&dir, &item.source, preamble).unwrap_or_default() 2264 + } 2265 + ReviewKind::Cloze => { 2266 + render_review_b64(&dir, &item.source, tala_typst::Preamble::ReviewCloze) 2267 + .unwrap_or_default() 2268 + } 2269 + ReviewKind::ImageRect { src, rect } => { 2270 + render_rect_question_b64(&dir, &item.source, &src, rect).unwrap_or_default() 2271 + } 2172 2272 }) 2173 2273 .await 2174 2274 .unwrap_or_default()