Merge pull request #4047 from dotCooCoo/fix/rasterize-threaded-text-keeps-its-text

Threaded text: Rasterize on a frame no longer duplicates its text
This commit is contained in:
Brandon Thomas
2026-10-25 10:37:15 -04:00
committed by GitHub
4 changed files with 52 additions and 2 deletions
+4 -1
View File
@@ -94,7 +94,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Rasterize…",
["Object"],
None,
"{ppi?, background?: transparent|white|black|\"#rrggbb\", antiAlias?: none|art|type (text snapped to pixels)|bool, padding?|addAround?: pt (0–1000), colorModel?: \"rgb\"|\"cmyk\"|\"grayscale\"|\"bitmap\", clippingMask?: bool (a clip group with the art's outline)} replace the selection with an embedded PNG image; each defaults to document.rasterEffectsSettings → {id (the image, or its clip group), width, height, warning?: \"an image couldn't be decoded and is missing from the picture\" (as raster exports say)}",
"{ppi?, background?: transparent|white|black|\"#rrggbb\", antiAlias?: none|art|type (text snapped to pixels)|bool, padding?|addAround?: pt (0–1000), colorModel?: \"rgb\"|\"cmyk\"|\"grayscale\"|\"bitmap\", clippingMask?: bool (a clip group with the art's outline)} replace the selection with an embedded PNG image; each defaults to document.rasterEffectsSettings; a frame of threaded text that is rasterized leaves its thread with the text it shows → {id (the image, or its clip group), width, height, warning?: \"an image couldn't be decoded and is missing from the picture\" (as raster exports say)}",
has_selection,
rasterize
),
@@ -561,8 +561,11 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(C, "nothing selected"));
}
let id = s.edit("Rasterize", |d, sel| {
let threaded = super::threads::frames(d);
let node = image.into_node(d);
let id = d.wrap(&roots, |_| node)?;
// Rasterized frames of threaded text leave their threads with the text they show.
super::threads::leave_threads(d, &threaded);
sel.set([id]);
Ok(id)
})?;
+44
View File
@@ -1176,6 +1176,50 @@ mod tests {
assert_eq!([a, b, c].map(|id| text(&s, id)), shown);
}
/// Object › Rasterize on frames of threaded text takes them out of their thread with the text
/// they show, which the image draws, and the other frames keep the text they hold. Undo threads
/// the frames again. When the last frame is rasterized, the frame before it takes the text it
/// has no room for.
#[test]
fn rasterized_frames_leave_their_thread_with_their_text() {
for clip in [false, true] {
for out in [&[0][..], &[1], &[2], &[0, 2]] {
let case = format!("clippingMask {clip} {out:?}");
let (mut s, frames) = three_frames(&TEXT.repeat(4), 200.0);
let shown = frames.map(|id| text(&s, id));
assert_eq!(shown.concat(), TEXT.repeat(4));
let ids: Vec<u64> = out.iter().map(|k| frames[*k]).collect();
s.execute("select.set", &json!({"ids": ids})).unwrap();
s.execute("object.rasterize", &json!({"ppi": 36, "clippingMask": clip})).unwrap();
let rest: Vec<u64> = frames.iter().copied().filter(|id| !ids.contains(id)).collect();
for id in &ids {
assert!(!is_frame(&s, *id), "{case}: frame {id} is rasterized");
}
for (id, before) in frames.iter().zip(&shown).filter(|(id, _)| rest.contains(id)) {
assert_eq!(text(&s, *id), *before, "{case}: frame {id} keeps its text");
}
assert_eq!(threads(&s), if rest.len() >= 2 { vec![rest] } else { vec![] }, "{case}");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(threads(&s), [frames], "{case}: undo");
assert_eq!(frames.map(|id| text(&s, id)), shown, "{case}: undo");
}
}
let (mut s, [a, b, c]) = three_frames(&TEXT.repeat(12), 100.0);
let shown = [a, b, c].map(|id| text(&s, id));
let laid = {
let Some(t) = text_of(&s.doc().unwrap().doc, NodeId(c)) else { panic!("not area type") };
vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t)
};
assert!(laid.overflow, "the last frame has more text than it shows");
let end = laid.lines.last().map_or(0, |l| l.end);
let hidden = shown[2][end..].trim_start_matches(' ');
s.execute("select.set", &json!({"ids": [c]})).unwrap();
s.execute("object.rasterize", &json!({"ppi": 36})).unwrap();
assert_eq!(text(&s, a), shown[0]);
assert_eq!(text(&s, b), format!("{}{hidden}", shown[1]), "the frame before it keeps its text and takes the rest");
assert_eq!(threads(&s), [[a, b]]);
}
/// Frame `id`'s first line: whether its first paragraph continues from the frame before, where
/// its text starts from the frame's left edge, and how many list markers it starts with.
fn first_line(s: &Session, id: u64) -> (bool, f64, usize) {