Threaded text: Flatten Transparency on a frame no longer duplicates its text

Object › Flatten Transparency replaced a frame of threaded text by its
outlines with Convert All Text to Outlines, and by flat regions or an
image where the frame was transparent. The thread's re-flow then added
the frame's text to the story, as it does for a frame that was deleted,
so the other frames held that text a second time while the outlines,
regions or image drew it.

A frame that Flatten Transparency outlines or flattens now leaves its
thread with the text it shows, and the other frames keep the text they
hold, as with Create Outlines and Expand. When the thread's last frame
is flattened, the text it has no room for goes after the text of the
last frame that stays. A thread left with one frame is no longer a
thread, and undo restores the thread.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-24 23:04:33 +07:00
committed by bflatastic
parent 34bd910a9b
commit ebd512d2a4
4 changed files with 61 additions and 3 deletions
+9 -2
View File
@@ -47,7 +47,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Flatten Transparency…",
["Object"],
None,
"{preset?: \"high\"|\"medium\"|\"low\" or a saved preset's name (see flattener.presets.list; default medium), balance?: 0..100 (raster/vector balance; 0 rasterizes everything), lineArtPpi?: 1..2400, gradientPpi?: 1..2400 (areas only gradients and meshes reach), textToOutlines?, strokesToOutlines?, clipComplexRegions? (clip images to the region outlines, else rectangles), antiAlias?, preserveAlpha? (composite over nothing instead of white), preserveOverprints? (areas showing one paint keep its colour and overprint; false clears overprints), options?: {the same keys}, ids?|id?} overlapping transparent objects become one group of flat-colour regions (CMYK colours composited ink by ink in CMYK documents), plus an image where gradients, patterns, images, masks or raster effects reach; objects without transparency stay → {ids, rasterized: images made, vector: regions made, options, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
"{preset?: \"high\"|\"medium\"|\"low\" or a saved preset's name (see flattener.presets.list; default medium), balance?: 0..100 (raster/vector balance; 0 rasterizes everything), lineArtPpi?: 1..2400, gradientPpi?: 1..2400 (areas only gradients and meshes reach), textToOutlines?, strokesToOutlines?, clipComplexRegions? (clip images to the region outlines, else rectangles), antiAlias?, preserveAlpha? (composite over nothing instead of white), preserveOverprints? (areas showing one paint keep its colour and overprint; false clears overprints), options?: {the same keys}, ids?|id?} overlapping transparent objects become one group of flat-colour regions (CMYK colours composited ink by ink in CMYK documents), plus an image where gradients, patterns, images, masks or raster effects reach; objects without transparency stay; a frame of threaded text that is outlined or flattened leaves its thread with the text it shows → {ids, rasterized: images made, vector: regions made, options, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
has_doc,
flatten
),
@@ -268,7 +268,14 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
if plan.flat.is_empty() && !plan.kept.iter().any(|id| src.node(*id).is_some_and(|n| kept_work(n, &o))) {
return Ok(json!({ "ids": roots.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": 0, "vector": 0, "options": options }));
}
let (ids, vector, rasterized) = s.edit("Flatten Transparency", |d, sel| apply(d, sel, plan, &o))?;
let (ids, vector, rasterized) = s.edit("Flatten Transparency", |d, sel| {
let threaded = super::threads::frames(d);
let out = apply(d, sel, plan, &o)?;
// Frames of threaded text that were outlined or flattened leave their threads with the
// text they show.
super::threads::leave_threads(d, &threaded);
Ok(out)
})?;
let mut out = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": rasterized, "vector": vector, "options": options });
if let Some(w) = warning {
out["warning"] = json!(w);
+49
View File
@@ -1066,6 +1066,55 @@ mod tests {
}
}
/// Object › Flatten Transparency outlines frames of threaded text with Convert All Text to
/// Outlines, and turns transparent frames into flat regions or an image: each such frame leaves
/// its thread with the text it shows, and the other frames keep the text they hold. Undo threads
/// the frames again. When the last frame is flattened, the frame before it takes the text it has
/// no room for.
#[test]
fn flattened_frames_leave_their_thread_with_their_text() {
let cases = [(json!({"textToOutlines": true}), false), (json!({}), true), (json!({"balance": 0}), true)];
for (options, transparent) in cases {
for out in [&[0][..], &[1], &[2], &[0, 2]] {
let case = format!("{options} {out:?}");
let (mut s, frames) = three_frames(&TEXT.repeat(4), 200.0);
let ids: Vec<u64> = out.iter().map(|k| frames[*k]).collect();
if transparent {
s.execute("transparency.set", &json!({"ids": ids, "opacity": 50})).unwrap();
}
let shown = frames.map(|id| text(&s, id));
assert_eq!(shown.concat(), TEXT.repeat(4));
let mut p = options.clone();
p["ids"] = json!(ids);
s.execute("object.flattenTransparency", &p).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 flattened");
}
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("object.flattenTransparency", &json!({"ids": [c], "textToOutlines": true})).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]]);
}
/// Expand Appearance keeps type whose only effect is a shadow or Crop Marks as type under a new
/// id, in a group under the frame's id: the type takes the frame's place in its thread, and
/// each frame keeps its text, the last frame the text it has no room for too. Type in a group
+2 -1
View File
@@ -4224,7 +4224,8 @@ frames keep the text they hold. When the thread's last frame is outlined, the te
outlines do not draw, goes after the text of the last frame that stays. A thread left with one frame is no longer a
thread. When `effect.expandAppearance` keeps a frame as type in a group (type with no fills or strokes of its own whose only
effects are shadows, outer glows or Crop Marks), the type stays in the thread in the frame's place. Object › Expand
(`object.expand`) outlines type with these two commands.
(`object.expand`) outlines type with these two commands. `object.flattenTransparency` takes each frame of threaded text
that it outlines (`textToOutlines`) or turns into flat regions or an image out of its thread in the same way.
## Converting between point type and area type
+1
View File
@@ -766,6 +766,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
A paragraph that continues from one threaded frame into the next has its first-line indent and bullet or number in the frame where it starts; in the next frame its first line starts where its wrapped lines start. In frames that are no longer threaded, copies of a frame and point type converted from one, the first paragraph has its indent and bullet or number again.
Type › Convert To Point Type on a frame of threaded text makes point type of the text that frame holds and takes it out of the thread; the story flows through the other frames without that text, so each character is in the document once.
Type › Create Outlines, Object › Expand and Object › Expand Appearance on a frame of threaded text take it out of the thread with the text it shows, which the outlines draw, and the other frames keep the text they hold. When the last frame is outlined, the text it has no room for goes after the text of the last frame that stays. When Expand Appearance keeps the frame as type in a group with its shadow, outer glow or Crop Marks, the type stays in the thread.
Object › Flatten Transparency takes a frame of threaded text out of the thread in the same way when Convert All Text to Outlines outlines it or when the frame becomes flat regions or an image, and the other frames keep the text they hold.
With Optical Margin Alignment on, or Roman Hanging Punctuation on in the first paragraph, the glyphs at the ends of left-to-right type on a path hang outside its brackets, measured along the path. The type hangs at the start bracket when its line is placed there, hangs at the end bracket when the line is placed there, and hangs at both when it is centered. Glyphs that hang past the end bracket do not make the type overflow, and a hang stops at an open path's end. SVG export writes such type as outlines.
The frames of threaded text share one Optical Margin Alignment setting: turning it on or off in one frame sets it in every frame, threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it.
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests_hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.