mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7803 from dotCooCoo/fix/create-outlines-drops-the-type-object-s-own
Type: Create Outlines keeps the type object's effects and transparency
This commit is contained in:
@@ -51,7 +51,7 @@ mod options;
|
||||
mod presets;
|
||||
mod report;
|
||||
|
||||
pub(crate) use analysis::{outlined_text, see_through_images};
|
||||
pub(crate) use analysis::{carry, outlined_text, see_through_images};
|
||||
pub(crate) use apply::{expand_transparent_tile_images, flatten_document, flatten_expanded, has_transparent_tile_images};
|
||||
#[cfg(test)]
|
||||
pub(crate) use apply::{with_clips_ignored, with_every_region};
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) fn outlined_text(n: &Node) -> Option<Node> {
|
||||
}
|
||||
|
||||
/// `to` (art made from `from`) with `from`'s name, transparency, opacity mask and effects.
|
||||
fn carry(from: &Node, mut to: Node) -> Node {
|
||||
pub(crate) fn carry(from: &Node, mut to: Node) -> Node {
|
||||
to.name = from.name.clone();
|
||||
to.opacity = from.opacity;
|
||||
to.blend = from.blend;
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Create Outlines",
|
||||
["Type"],
|
||||
Some("Cmd+Shift+O"),
|
||||
"{} convert the selected text to groups of compound paths (one per glyph); the type object's own fills and strokes (not its characters') go on paths of its whole outline, below or above the glyphs as its Characters row places them; a frame of threaded text leaves its thread with the text it shows; a dimension whose label is outlined becomes a plain group of its art → {ids}",
|
||||
"{} convert the selected text to groups of compound paths (one per glyph); the type object's own fills and strokes (not its characters') go on paths of its whole outline, below or above the glyphs as its Characters row places them; the group keeps the object's transparency, opacity mask and effects, once; a frame of threaded text leaves its thread with the text it shows; a dimension whose label is outlined becomes a plain group of its art → {ids}",
|
||||
has_selection,
|
||||
create_outlines
|
||||
),
|
||||
@@ -739,11 +739,13 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
if children.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// The group takes the object's name, transparency, opacity mask and effects, which
|
||||
// apply to the type as a whole: once, not to each glyph. It is marked as type's
|
||||
// outlines, so geometry effects move its placed gradients with the whole outline, as
|
||||
// they do on the type.
|
||||
let gid = d.alloc_id();
|
||||
let mut g = Node::group(gid, children);
|
||||
g.opacity = n.opacity;
|
||||
g.blend = n.blend;
|
||||
g.name = n.name.clone();
|
||||
let mut g = super::flatten::carry(&n, Node::group(gid, children));
|
||||
g.extras_mut().type_outlines = true;
|
||||
d.insert(par, idx, g)?;
|
||||
out.push(gid);
|
||||
if roots.contains(&tid) {
|
||||
|
||||
@@ -775,6 +775,44 @@ fn create_outlines_draws_the_type_objects_own_paint() {
|
||||
assert!(differing(&before, &render(&session()), 24) > 1000, "the type paints something");
|
||||
}
|
||||
|
||||
/// Create Outlines draws what the type drew with effects of its own: a drop shadow of the whole
|
||||
/// type and a Transform effect that scales and turns it, over fills and strokes of the object's
|
||||
/// own around its Characters row.
|
||||
#[test]
|
||||
fn create_outlines_draws_the_type_objects_own_effects() {
|
||||
let mut s = session();
|
||||
type_with_own_paint(&mut s, &[red_blue(), blue_stroke()], 1);
|
||||
let plain = render(&s);
|
||||
let shadow = json!({"x": 6, "y": 6, "blur": 0, "opacity": 100, "mode": "normal", "color": "#00ff00"});
|
||||
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "params": shadow, "item": null}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.transform", "params": {"scaleH": 80, "scaleV": 80, "rotate": 10}, "item": null}));
|
||||
let before = render(&s);
|
||||
assert!(differing(&plain, &before, 24) > 500, "the effects change what the type draws");
|
||||
run(&mut s, "type.createOutlines", json!({}));
|
||||
let after = render(&s);
|
||||
let n = differing(&before, &after, 24);
|
||||
assert!(n < 40, "{n} pixels differ");
|
||||
}
|
||||
|
||||
/// A Warp effect of the type object's own bends one gradient across the whole type on the canvas,
|
||||
/// and the group Create Outlines makes keeps it across the whole outline rather than one per
|
||||
/// glyph, so the outlines draw what the type drew.
|
||||
#[test]
|
||||
fn create_outlines_keeps_a_warped_character_gradient_across_the_type() {
|
||||
let mut s = session();
|
||||
let t = run(&mut s, "text.create", json!({"x": 20, "y": 110, "text": "HIH", "size": 60}))["id"].as_u64().unwrap();
|
||||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||||
let down = json!({"kind": "linear", "angle": 90, "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]});
|
||||
run(&mut s, "paint.setFill", json!({"gradient": down}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "warp.arc", "params": {"bend": 50}, "item": null}));
|
||||
let before = render(&s);
|
||||
run(&mut s, "type.createOutlines", json!({}));
|
||||
let after = render(&s);
|
||||
let n = differing(&before, &after, 24);
|
||||
assert!(n < 40, "{n} pixels differ");
|
||||
}
|
||||
|
||||
/// Selected type "HIH" at 40 pt with ruby, underlined and struck through, its characters filled
|
||||
/// with `fill` (a `paint.setFill` param) and stroked 2 pt wide, then scaled to 150% and rotated 20
|
||||
/// degrees (the stroke keeps its 2 pt, which is 4/3 pt in text space).
|
||||
|
||||
@@ -1088,6 +1088,62 @@ fn create_outlines_keeps_the_objects_own_fills_and_strokes() {
|
||||
assert_eq!(node(&s, t).appearance.items, own, "one undo step brings the type back");
|
||||
}
|
||||
|
||||
/// The type object's own effects go on the group once, in their order, with their settings and a
|
||||
/// hidden one still hidden; a fill's own effect stays on its path of the whole outline, and no
|
||||
/// glyph or whole-outline path takes the object's effects. Undo brings the type back with them.
|
||||
#[test]
|
||||
fn create_outlines_keeps_the_objects_own_effects_on_the_group() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Hi", 64.0);
|
||||
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
|
||||
for (cmd, p) in [
|
||||
("appearance.addFill", json!({"target": "object"})),
|
||||
("appearance.setItem", json!({"index": 0, "color": "#ff0000"})),
|
||||
("effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 2}, "item": 0})),
|
||||
("effect.apply", json!({"effect": "stylize.dropShadow", "params": {"x": 7, "y": 3, "blur": 0, "opacity": 60}, "item": null})),
|
||||
("effect.apply", json!({"effect": "distort.transform", "params": {"rotate": 15}, "item": null})),
|
||||
("effect.setParams", json!({"index": 1, "visible": false, "item": null})),
|
||||
] {
|
||||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"));
|
||||
}
|
||||
let own: Appearance = (*node(&s, t).appearance).clone();
|
||||
let ids_of = |fx: &[vectorcraft_doc::Effect]| fx.iter().map(|e| (e.id.clone(), e.visible)).collect::<Vec<_>>();
|
||||
assert_eq!(ids_of(&own.effects), [("stylize.dropShadow".to_string(), true), ("distort.transform".to_string(), false)]);
|
||||
assert_eq!(ids_of(own.items[0].effects()), [("path.offsetPath".to_string(), true)]);
|
||||
let r = ids(&s.execute("type.createOutlines", &json!({})).unwrap());
|
||||
let g = node(&s, r[0]);
|
||||
assert_eq!(g.appearance.effects, own.effects, "the group holds the object's effects as they were");
|
||||
let ch = g.children().unwrap();
|
||||
assert!(ch.iter().all(|c| c.appearance.effects.is_empty()), "no piece takes the object's effects");
|
||||
let rows: Vec<_> = ch.iter().filter(|c| c.appearance.items == own.items).collect();
|
||||
assert_eq!(rows.len(), 1, "one path of the whole outline paints the object's fill");
|
||||
assert_eq!(rows[0].appearance.items[0].effects(), own.items[0].effects(), "the fill keeps its own effect");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*node(&s, t).appearance, own, "one undo step brings the type back with its effects");
|
||||
}
|
||||
|
||||
/// The group takes the type object's opacity, blend mode, Isolate Blending, Opacity & Mask Define
|
||||
/// Knockout Shape and opacity mask.
|
||||
#[test]
|
||||
fn create_outlines_keeps_the_objects_transparency_and_opacity_mask() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Hi", 64.0);
|
||||
let m = rect(&mut s, 0.0, 0.0, 40.0, 80.0);
|
||||
s.execute("select.set", &json!({"ids": [t.0, m.0]})).unwrap();
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
let p = json!({"ids": [t.0], "opacity": 70, "blend": "multiply", "isolate": true, "knockoutShape": true});
|
||||
s.execute("transparency.set", &p).unwrap();
|
||||
let before = node(&s, t);
|
||||
assert!(before.extras().mask.is_some(), "the type has a mask");
|
||||
let look = |n: &vectorcraft_doc::Node| (n.opacity, n.blend, n.isolate, n.knockout_shape);
|
||||
assert_eq!(look(&before), (0.7, vectorcraft_color::BlendMode::Multiply, true, true));
|
||||
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
|
||||
let r = ids(&s.execute("type.createOutlines", &json!({})).unwrap());
|
||||
let g = node(&s, r[0]);
|
||||
assert_eq!(look(&g), look(&before));
|
||||
assert_eq!(g.extras().mask, before.extras().mask, "the group keeps the type's opacity mask");
|
||||
}
|
||||
|
||||
/// Colour bitmap glyphs (emoji) become embedded images where they sat; glyphs showing one image
|
||||
/// share its pixels.
|
||||
#[test]
|
||||
|
||||
@@ -9054,6 +9054,11 @@ Appearance panel's Characters row, not the characters' formatting) go on paths o
|
||||
below the Characters row on one path under the glyph paths, the others on one path over them, in their order and with
|
||||
their opacity, blend mode, effects and stroke settings. Their gradients that are not placed are fixed to the type's
|
||||
layout box, as the canvas paints them, so one gradient spans every glyph. The group itself has no fills or strokes.
|
||||
It takes the type object's name, opacity, blend mode, Isolate Blending, Opacity & Mask Define Knockout Shape, opacity
|
||||
mask and effects. The effects (the object's own, which apply to the type as a whole) keep their order, settings and
|
||||
hidden state and stay live on the group; none of them is copied onto the glyph paths. Geometry effects on the group
|
||||
(Warp, Free Distort, Transform) move its placed gradients with the whole outline, as they do on the type, not with each
|
||||
glyph.
|
||||
`object.expand` outlines type with this command, so Fill and Stroke expand those paths' gradients, patterns and strokes.
|
||||
|
||||
`type.createOutlines` (Type › Create Outlines) and `effect.expandAppearance` (when it outlines type, as Expand
|
||||
|
||||
@@ -699,6 +699,7 @@ since 2026-10-01.
|
||||
- **COLR colour fonts:** v0 and v1 glyphs draw as vector layers (solid colours, linear and radial gradients, including ones that repeat or reflect, clips, blend modes, the foreground colour from the run's fill) on the canvas, as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as Type 3 text in PDF, and as editable groups with Create Outlines; sweep gradients, Porter-Duff compositing and radial gradients that repeat or reflect and start at a circle draw the glyph as an image. Measured by `tests_colr`, `render::tests_colrglyphs`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colr_glyphs_*`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups`. Scores unchanged (estimated).
|
||||
- **OpenType SVG colour fonts:** each glyph's SVG document is read by the SVG importer (registered by the engine) into vector art in the run's colours, then drawn, exported and outlined as COLR glyphs are; PDF real text writes them as text that draws nothing and draws their art where they sit. Measured by `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`. Scores unchanged (estimated).
|
||||
- **Create Outlines keeps the type object's own appearance:** Type › Create Outlines and Object › Expand put the type object's own fills and strokes (the Appearance panel's items around its Characters row) on paths of the whole outline, below or above the glyph paths as that row places them, with one gradient across every glyph, so the outlines draw what the type drew; `object.expand.info` reports those strokes, pattern fills and gradient fills. Outlined characters keep their stroke and fill (the help page, <https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=337>); a type object's appearance is separate from its characters' (the help page, <https://helpx.adobe.com/illustrator/using/appearance-attributes.html>); where the object's items go is VectorCraft's own rule. Measured by `tests::pathops::create_outlines_keeps_the_objects_own_fills_and_strokes`, `tests::objexpand::create_outlines_draws_the_type_objects_own_paint`, `tests::objexpand::type_objects_own_stroke_pattern_and_gradient_are_reported`, `tests::objexpand::type_without_an_outline_reports_no_own_stroke_or_fill`, `tests::objexpand::a_stroke_under_an_unpainted_stroke_is_reported_and_outlined` and `tests::objexpand::expand_keeps_the_type_objects_own_paint`. Scores unchanged (estimated).
|
||||
- **Create Outlines keeps the type object's own effects and transparency:** Type › Create Outlines gives the group of outlines the type object's own effects, once, in their order, with their settings and hidden effects still hidden, so they stay live and editable and the outlines draw what the type drew (a Warp or Free Distort effect moves a gradient on the characters across the whole outline, as on the type); the effects of the object's own fills and strokes stay on their paths of the whole outline. The group also takes the object's opacity, blend mode, Isolate Blending, Opacity & Mask Define Knockout Shape and opacity mask. Outlined characters keep their stroke and fill (the help page, <https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=365>); putting the object's effects on the group is VectorCraft's own rule. Measured by `tests::pathops::create_outlines_keeps_the_objects_own_effects_on_the_group`, `tests::pathops::create_outlines_keeps_the_objects_transparency_and_opacity_mask`, `tests::objexpand::create_outlines_draws_the_type_objects_own_effects` and `tests::objexpand::create_outlines_keeps_a_warped_character_gradient_across_the_type`. Scores unchanged (estimated).
|
||||
- **Create Outlines keeps the characters' gradients across the type:** Type › Create Outlines and Object › Expand give every glyph, ruby glyph and underline or strikethrough bar the gradient its characters drew with: a gradient with no vector of its own stays fitted to the whole layout rather than to each glyph, and one with a vector keeps its place, both moved, scaled and rotated with the type; the characters' stroke keeps the weight it was drawn at on scaled type. `object.expand.info` measures gradient fills on the same paths. Outlined characters keep their stroke and fill (the help page, <https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=337>); the gradient fitted across the whole layout is VectorCraft's own rule. Measured by `tests::objexpand::create_outlines_keeps_a_character_gradient_across_the_type` and `tests::objexpand::expand_keeps_a_character_gradient_across_the_type`. Scores unchanged (estimated).
|
||||
- **Composite fonts that two documents define differently:** when two open documents each define a composite font (Type › Composite Fonts…) by the same name with different parts, each document draws and exports its type in its own definition: in a tiled or floating window drawn while the front document draws, when it is placed in a document that defines the name differently, and in an export made while another document is in front. Evidence: `tests::composite::nested_loans_of_one_name_use_each_documents_definition` and `threads_lending_one_name_at_once_use_their_own_definitions` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` (`crates/render`). Scores unchanged (estimated).
|
||||
- **Composite fonts only another document defines:** a document draws, measures and exports its type only in the composite fonts it defines itself. Type naming a composite font that only another open document, or the document a placed document sits in, defines is set as in a missing font, also when the document defines no composite font: in a window, a thumbnail or an export of a document in the back, in a file being opened and in a placed document. A temporary document drawn while a document draws keeps that document's composite fonts. Evidence: `tests::composite::a_lent_document_does_not_use_another_documents_composite_font` and `a_document_without_composite_fonts_uses_none_and_a_preview_is_unchanged` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` and `a_document_draws_type_in_another_documents_composite_font_as_missing` (`crates/render`), `a_document_replaced_in_the_back_measures_a_font_it_lacks_as_missing` (`crates/engine`). Scores unchanged (estimated).
|
||||
|
||||
Reference in New Issue
Block a user