From 543034987eb6d2e31f73a6b373234c40e9802263 Mon Sep 17 00:00:00 2001 From: jmverges Date: Wed, 11 Nov 2026 19:33:22 +0000 Subject: [PATCH] Envelopes: type's strokes paint at their own weight along the bent outlines (#6644) With Envelope Options > Distort Appearance on, VectorCraft turned the strokes of type in an envelope into filled outlines and bent them with the art, so a stroke got thinner where the grid squeezes the type and thicker where it stretches it. The drawn art of users' .ai files keeps those strokes as stroked paths at the type's own weights, and the ring they paint is as wide wherever the grid squeezes or stretches the type. An envelope of stroked type in such a grid therefore didn't draw as its drawn art and kept it instead of opening live. Type's strokes (its characters' and those on its own appearance) now paint along the bent glyph outlines at their own weight, with Distort Appearance on or off, also in an envelope inside an envelope: - doc: `map_node` maps the groups that hold type's glyph outlines with `Hooks::type_appearance` in place of `Hooks::appearance`, for type in the envelope's contents and for type that a live object in them (an inner envelope) outlined (`Warper::type_outlines`). Such a group's opacity mask is mapped with the ordinary hooks, so it bends as other art does. Groups marked as type's outlines in the contents themselves (art an earlier Expand made) stay plain art. - effects: `bake_type_appearance`, `bake_appearance` with strokes kept as strokes: geometry effects on type's fills and strokes still bake in and bend. - doc: `max_outset` counts type's character strokes as type's own visual bounds do (`char_stroke_outset`). It left them out, though they already painted past an envelope's grid where the glyphs reach its edge; this also grows the visual bounds of blends and repeats of stroked type and the reach of symbol art holding it. Paths' strokes, geometry effects, opacity masks, linear gradients and pattern fills bend as before. The canvas, hit testing, effect bounds, Expand, every export and the .ai save take the evaluation from the same hooks. Expand of an envelope of stroked type gives the bent outlines with the strokes on them. This changes how VectorCraft's own envelopes of stroked type draw. Tests: envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope, a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope, type_strokes_paint_at_their_own_weight_in_an_envelope_inside_an_envelope, a_paths_stroke_still_bends_in_a_squeezed_envelope, an_opacity_mask_on_type_bends_as_other_art, the_visual_bounds_of_an_envelope_hold_its_types_strokes, the_visual_bounds_of_a_repeat_of_stroked_type_grow_by_half_the_stroke, expanding_an_envelope_of_stroked_type_keeps_the_stroke_at_its_weight, bake::tests::type_pieces_bake_their_geometry_effects_and_keep_their_strokes and tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/live/envelope.rs | 5 +- crates/doc/src/live/expand.rs | 35 +++- crates/effects/src/bake.rs | 29 ++- crates/effects/src/bake/tests.rs | 21 ++ crates/effects/src/lib.rs | 2 +- crates/effects/src/live.rs | 10 +- crates/engine/src/tests/envelope_distort.rs | 207 ++++++++++++++++++++ crates/eps/src/import/tests_native.rs | 100 +++++++++- docs/file-format-parity.md | 3 +- docs/mcp.md | 10 +- docs/target-app-parity.md | 6 +- docs/type-parity.md | 3 +- 12 files changed, 406 insertions(+), 25 deletions(-) diff --git a/crates/doc/src/live/envelope.rs b/crates/doc/src/live/envelope.rs index 8f9b197048..8abe961841 100644 --- a/crates/doc/src/live/envelope.rs +++ b/crates/doc/src/live/envelope.rs @@ -78,7 +78,9 @@ pub struct EnvelopeOptions { /// Rasters: Preserve Shape Using. pub preserve_shape: PreserveShape, /// Distort Appearance: strokes and live effects are applied before the distortion, so they - /// bend with the art (off: they apply to the distorted paths). + /// bend with the art (off: they apply to the distorted paths). Type's strokes are the exception: + /// they paint along its distorted glyph outlines at their own weight either way, as `.ai` files + /// draw them (#6644). pub distort_appearance: bool, /// Distort Linear Gradients (with Distort Appearance): placed gradients bend with the art. pub distort_linear_gradients: bool, @@ -638,6 +640,7 @@ pub(super) fn expand_envelope( hooks, options, depth: 0, + type_outlines: false, }; content.iter().map(|c| map_node(c, &w)).collect() } diff --git a/crates/doc/src/live/expand.rs b/crates/doc/src/live/expand.rs index 1cc8ba0863..8675dcc011 100644 --- a/crates/doc/src/live/expand.rs +++ b/crates/doc/src/live/expand.rs @@ -21,6 +21,11 @@ pub struct Hooks<'a> { /// Distort Appearance: a path's strokes and geometry effects baked into filled art (`None` /// when it has none to bake). pub appearance: Outliner<'a>, + /// Distort Appearance for the pieces of type's glyph outlines: their geometry effects baked + /// as [`Hooks::appearance`] bakes them, their strokes left as strokes, which then paint along + /// the bent outlines at their own weight (#6644). `None` when a piece has nothing to bake, and + /// without this hook nothing in type's outlines is baked. + pub type_appearance: Outliner<'a>, /// Distort Pattern Fills: the tiles a pattern fill lays (see [`PatternTiles`]). pub pattern: Option>, /// The document whose symbols' art counts toward the instance budget of a Repeat of their @@ -61,6 +66,12 @@ pub(super) struct Warper<'a> { pub(super) options: EnvelopeOptions, /// How many hooks the art being mapped came through. pub(super) depth: u32, + /// Are groups marked as type's glyph outlines ([`crate::NodeExtras::type_outlines`]) in the art + /// being mapped type that live evaluation outlined: the type of the envelope's contents, or of + /// a live object in them, such as an envelope inside the envelope? Their strokes then paint + /// along the bent outlines at their own weight (#6644). Groups so marked in the contents as + /// they are, art an earlier Expand made of type, are plain art. + pub(super) type_outlines: bool, } impl Warper<'_> { @@ -117,6 +128,13 @@ pub(super) fn map_node(n: &Node, w: &Warper) -> Node { *live = None; } NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => { + // Type's glyph outlines: with Distort Appearance too, their strokes paint along the + // bent outlines at their own weight, as `.ai` files draw them (#6644). Their pieces are + // baked by `type_appearance`, which keeps strokes as strokes; their geometry effects, + // gradients and pattern fills follow the options as a path's do. The group's opacity + // mask, mapped above, bends as other art does. + let typed = Warper { hooks: Hooks { appearance: w.hooks.type_appearance, ..w.hooks }, ..*w }; + let w = if w.type_outlines && n.extras().type_outlines { &typed } else { w }; for c in children.iter_mut() { *c = Arc::new(map_node(c, w)); } @@ -124,14 +142,17 @@ pub(super) fn map_node(n: &Node, w: &Warper) -> Node { NodeKind::Mesh(m) => m.map(w.f), NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => { if let Some(d) = &deeper { - return map_node(&expanded_group_hooks(n, w.hooks), d); + // The type's outlines in its art are type (an envelope inside the envelope, #6644). + return map_node(&expanded_group_hooks(n, w.hooks), &Warper { type_outlines: true, ..*d }); } } NodeKind::Text(_) | NodeKind::SymbolInstance { .. } => { if let Some(d) = &deeper && let Some(o) = w.hooks.outline.and_then(|h| h(n)) { - return map_node(&carry_look(n, o), d); + // Type's outlines are type (#6644, the group arm above). + let d = Warper { type_outlines: d.type_outlines || matches!(n.kind, NodeKind::Text(_)), ..*d }; + return map_node(&carry_look(n, o), &d); } } NodeKind::Image(im) => { @@ -439,8 +460,11 @@ pub fn subtree_has_live(n: &Node) -> bool { any } -/// Largest stroke outset in a subtree (for visual bounds of envelopes). With `symbols`, the -/// outsets in the art of the symbols its instances show count too +/// Largest stroke outset in a subtree (for visual bounds of envelopes, blends and repeats, and the +/// reach of symbol art), type's character strokes included as type's own visual bounds measure them +/// ([`crate::TextObject::char_stroke_outset`]): in an envelope they paint at their own weight +/// around the bent glyphs, past its grid where the glyphs reach its edge (#6644). With +/// `symbols`, the outsets in the art of the symbols its instances show count too /// ([`crate::symbols::ArtBox::reach`]), grown by [`crate::SymbolBoxes::brush_pad`]. In boxes made /// by [`crate::SymbolBoxes::without_art_strokes`], the outsets in the art do not count, and the pad /// does. @@ -449,6 +473,9 @@ pub(crate) fn max_outset(nodes: &[Arc], symbols: Option<&crate::SymbolBoxe for n in nodes { n.walk(&mut |c| { o = o.max(c.appearance.outset()); + if let NodeKind::Text(t) = &c.kind { + o = o.max(t.char_stroke_outset()); + } if let (Some(s), NodeKind::SymbolInstance { symbol, .. }) = (symbols, &c.kind) && let Some(art) = s.art(symbol) { diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 5d27ac60af..795fc69bdf 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -552,6 +552,21 @@ pub fn expand_art(d: &mut Document, n: &mut Node, stroke_art: StrokeArt) { /// painting their pieces name are looked up in `doc`. `None` when it has neither a stroke to /// outline nor a geometry effect, so bending first changes nothing. pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option { + bake_appearance_as(doc, n, true) +} + +/// [`bake_appearance`] for the glyph outlines of type in an envelope with Distort Appearance: +/// geometry effects are applied as it applies them, and fills, gradients and patterns are left as +/// it leaves them, but every stroke stays a stroke on its piece instead of becoming its filled +/// outline. The envelope then bends the piece's path, and the stroke paints along the bent outline +/// at its own weight, as `.ai` files draw the strokes of type in an envelope (#6644). `None` when +/// it has no geometry effect, so bending first changes nothing. +pub fn bake_type_appearance(doc: Option<&Document>, n: &Node) -> Option { + bake_appearance_as(doc, n, false) +} + +/// [`bake_appearance`], or with `outline_strokes` false [`bake_type_appearance`]. +fn bake_appearance_as(doc: Option<&Document>, n: &Node, outline_strokes: bool) -> Option { if let Some(m) = crate::revolve_art_in(doc, n).or_else(|| crate::solid_art_in(doc, n)) { return Some(m); } @@ -564,10 +579,10 @@ pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option { if crate::has_transform_art(n) && let Some(mut m) = reshape(doc, n, None) { - bake_appearance_pieces(doc, &mut m); + bake_appearance_pieces(doc, &mut m, outline_strokes); return Some(m); } - let outlined = |i: &AppearanceItem| paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none()); + let outlined = |i: &AppearanceItem| outline_strokes && paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none()); let geometric = has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_geometry(item_effects(i))); if !geometric && !n.appearance.items.iter().any(outlined) { return None; @@ -585,7 +600,7 @@ pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option { let (ig, mut it) = apply_item(item, carried, &g, &ctx); clear_item_effects(&mut it); pieces.push(match &it { - AppearanceItem::Stroke(st) if st.brush.is_none() => { + AppearanceItem::Stroke(st) if outline_strokes && st.brush.is_none() => { let outline = crate::stroke::outline_region(&ig, rule, st); match st.path_gradient() { // A gradient along or across the stroke: its gradient meshes clipped to the outline. @@ -618,16 +633,16 @@ pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option { Some(m) } -/// [`bake_appearance`] every path and compound path in `n` that it changes, in place. -fn bake_appearance_pieces(doc: Option<&Document>, n: &mut Node) { +/// [`bake_appearance_as`] every path and compound path in `n` that it changes, in place. +fn bake_appearance_pieces(doc: Option<&Document>, n: &mut Node, outline_strokes: bool) { if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) { - if let Some(m) = bake_appearance(doc, n) { + if let Some(m) = bake_appearance_as(doc, n, outline_strokes) { *n = m; } return; } for c in n.children_mut().into_iter().flatten() { - bake_appearance_pieces(doc, Arc::make_mut(c)); + bake_appearance_pieces(doc, Arc::make_mut(c), outline_strokes); } } diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index 4751b137c9..2bcc026340 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -575,6 +575,27 @@ fn a_baked_stroke_with_a_gradient_along_it_becomes_meshes_clipped_to_its_outline assert_eq!(ch[1].opacity, 0.5); } +/// Distort Appearance on the pieces of type's glyph outlines (#6644): a stroke's geometry effect +/// still bakes into its piece, but the stroke stays a stroke at its own weight; a piece with no +/// geometry effect is left as it is, its stroke on it (Distort Appearance on a path outlines it). +#[test] +fn type_pieces_bake_their_geometry_effects_and_keep_their_strokes() { + let mut n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art()); + assert!(bake_type_appearance(None, &n).is_none(), "nothing to bake"); + assert!(bake_appearance(None, &n).is_some(), "a path's stroke is outlined"); + if let AppearanceItem::Stroke(s) = &mut n.appearance.items[1] { + s.width = 4.0; + s.effects.push(fx("path.offsetPath", json!({"offset": 5.0}))); + } + let m = bake_type_appearance(None, &n).unwrap(); + let ch = m.children().unwrap(); + assert_eq!(ch.len(), 2); + let Some(AppearanceItem::Stroke(st)) = ch[1].appearance.items.first() else { panic!("still a stroke: {:?}", ch[1].appearance) }; + assert!(st.width == 4.0 && st.effects.is_empty(), "{st:?}"); + assert!((ch[1].geometric_bounds().unwrap().width() - 110.0).abs() < 0.5, "the stroke's offset is baked into its path"); + assert!((ch[0].geometric_bounds().unwrap().width() - 100.0).abs() < 1e-6, "the fill keeps the square"); +} + /// A 100 pt square at the origin painted by `fill` and `stroke` (`width` pt), with the Transform /// effect `params`. fn transformed(fill: Paint, stroke: Paint, width: f64, params: serde_json::Value) -> Node { diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index ae8b7d9e90..67b4b464fd 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -68,7 +68,7 @@ pub use adjust::{ ADJUSTMENTS, CHANNELS, ColorMap, ImageHook, adjust, adjust_in_document, color_map, curve_at, curve_points, has_adjustment, is_adjustment, is_channel, }; -pub use bake::{StrokeArt, bake_appearance, bake_document, expand_art, expand_leaf, fresh_ids, needs_bake}; +pub use bake::{StrokeArt, bake_appearance, bake_document, bake_type_appearance, expand_art, expand_leaf, fresh_ids, needs_bake}; pub use clip::clip_outline; pub use group::{ OutlineHook, PATHFINDER_EFFECTS, compound_shape_area, compound_shape_art, evaluate_container, has_container_appearance, has_pathfinder, diff --git a/crates/effects/src/live.rs b/crates/effects/src/live.rs index 03b335f010..2c02f886c0 100644 --- a/crates/effects/src/live.rs +++ b/crates/effects/src/live.rs @@ -2,8 +2,9 @@ //! every exporter, with what the pure evaluation in [`vectorcraft_doc::live`] can't do itself //! ([`Hooks`]): type inside an envelope distorts as its glyph outlines, run by run //! ([`outline_text`]); symbol instances as their symbol's art; with Distort Appearance strokes and -//! geometry effects bend with the art ([`bake_appearance`]); with Distort Pattern Fills the pattern -//! tiles do. +//! geometry effects bend with the art ([`bake_appearance`]), except type's strokes, which paint +//! along its bent outlines at their own weight ([`bake_type_appearance`], #6644); with Distort +//! Pattern Fills the pattern tiles do. use std::sync::Arc; @@ -11,7 +12,7 @@ use vectorcraft_doc::live::{self, Hooks, Outliner}; use vectorcraft_doc::{Document, Node, NodeKind}; use vectorcraft_geom::{Affine, Rect}; -use crate::{bake_appearance, outline_text}; +use crate::{bake_appearance, bake_type_appearance, outline_text}; /// The text outliner live evaluation uses: type as its glyph outlines, one path per run in the /// run's paint ([`outline_text`]). @@ -44,7 +45,8 @@ fn with_hooks(doc: Option<&Document>, f: impl FnOnce(Hooks) -> R) -> R { let outline = |n: &Node| outline_object(doc, n); let tiles = |name: &str, xf: Affine, region: Rect| Some(doc?.pattern(name)?.instances_in(xf, region)); let appearance = |n: &Node| bake_appearance(doc, n); - f(Hooks { outline: Some(&outline), appearance: Some(&appearance), pattern: Some(&tiles), symbols: doc }) + let type_appearance = |n: &Node| bake_type_appearance(doc, n); + f(Hooks { outline: Some(&outline), appearance: Some(&appearance), type_appearance: Some(&type_appearance), pattern: Some(&tiles), symbols: doc }) } /// One level of evaluation of live object `n` ([`live::expand_live_hooks`]). diff --git a/crates/engine/src/tests/envelope_distort.rs b/crates/engine/src/tests/envelope_distort.rs index b42b1da027..8a1e46c68a 100644 --- a/crates/engine/src/tests/envelope_distort.rs +++ b/crates/engine/src/tests/envelope_distort.rs @@ -405,3 +405,210 @@ fn new_envelopes_distort_appearance_and_old_ones_keep_their_look() { let e = arch(&mut s, 50.0, json!({})); assert!(matches!(node(&s, e).kind, NodeKind::Envelope { options, .. } if options == EnvelopeOptions::NEW)); } + +/// What strokes the object in [`squeezed`]. +#[derive(Clone, Copy)] +enum Stroked { + /// Nothing. + No, + /// The characters of type, or a path's own stroke. + Own, + /// A stroke on type's own appearance (Appearance panel), the characters unstroked. + Appearance, +} + +/// The weight of the strokes in [`squeezed`]. +const SQUEEZED_WEIGHT: f64 = 8.0; + +/// `make`'s object, stroked by `stroked` with a red stroke [`SQUEEZED_WEIGHT`] wide, in a one-patch +/// mesh envelope with Distort Appearance on whose right column is moved in to squeeze the object to +/// half its width (a horizontal squeeze, the same everywhere): the session, the envelope and the +/// height of the envelope's middle. +fn squeezed(make: fn(&mut Session) -> NodeId, stroked: Stroked) -> (Session, NodeId, f64) { + use vectorcraft_doc::live::EnvelopeKind; + let mut s = session(); + let a = make(&mut s); + sel(&mut s, &[a]); + match stroked { + Stroked::No => {} + Stroked::Own => { + s.execute("paint.setStroke", &json!({"ids": [a.0], "color": "#ff0000"})).unwrap(); + s.execute("stroke.set", &json!({"ids": [a.0], "weight": SQUEEZED_WEIGHT})).unwrap(); + } + Stroked::Appearance => { + s.edit("appearance stroke", |d, _| { + let n = d.node_mut(a).unwrap(); + let mut appearance = (*n.appearance).clone(); + appearance + .items + .push(AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), SQUEEZED_WEIGHT))); + n.appearance = appearance.into(); + Ok(()) + }) + .unwrap(); + } + } + let e = id_of(&s.execute("object.envelope.makeWithMesh", &json!({"rows": 1, "cols": 1})).unwrap()); + let NodeKind::Envelope { options, .. } = &node(&s, e).kind else { panic!("an envelope") }; + assert!(options.distort_appearance, "new envelopes distort appearance"); + let mut middle = 0.0; + s.edit("squeeze", |d, _| { + let NodeKind::Envelope { kind, .. } = &mut d.node_mut(e).unwrap().kind else { panic!("an envelope") }; + let EnvelopeKind::Mesh { points, handles, .. } = &mut **kind else { panic!("a mesh") }; + let (left, right) = (points[0].x, points[1].x); + points[1].x = left + (right - left) / 2.0; + points[3].x = points[1].x; + handles.clear(); + middle = (points[0].y + points[2].y) / 2.0; + Ok(()) + }) + .unwrap(); + (s, e, middle) +} + +/// Where the ink of the page of `s` starts and ends across height `y`, as the canvas draws it. +fn ink_across(s: &Session, y: f64) -> (f64, f64) { + let scale = 8.0; + let region = Rect::new(0.0, y - 0.5, 800.0, y + 0.5); + let r = vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, region, scale, true); + let row = (r.height / 2) as usize; + let inked: Vec = (0..r.width as usize).filter(|i| r.pixels[(row * r.width as usize + i) * 4..][..3].iter().any(|c| *c < 128)).collect(); + let (first, last) = (inked.first().unwrap(), inked.last().unwrap()); + (*first as f64 / scale, (last + 1) as f64 / scale) +} + +/// How wide the ink is across the middle of `make`'s object in [`squeezed`]. +fn squeezed_ink(make: fn(&mut Session) -> NodeId, stroked: Stroked) -> f64 { + let (s, _, middle) = squeezed(make, stroked); + let (first, last) = ink_across(&s, middle); + last - first +} + +/// A capital I set 300 points tall at (100, 400), filled black. +fn tall_i(s: &mut Session) -> NodeId { + id_of(&s.execute("text.create", &json!({"x": 100, "y": 400, "text": "I", "size": 300})).unwrap()) +} + +/// With Distort Appearance on, the characters' stroke of type in an envelope paints along the bent +/// glyph outlines at its own weight, as `.ai` files draw it (#6644): squeezed to half its width, the +/// stem of an I still has half the weight of stroke on each side, not a quarter. +#[test] +fn type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope() { + let (plain, stroked) = (squeezed_ink(tall_i, Stroked::No), squeezed_ink(tall_i, Stroked::Own)); + let past = stroked - plain; + assert!((past - SQUEEZED_WEIGHT).abs() < 0.5, "the stroke reaches {past} pt past the squeezed stem, both sides together ({plain} → {stroked})"); +} + +/// A stroke on type's own appearance paints at its own weight too (#6644). +#[test] +fn a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope() { + let (plain, stroked) = (squeezed_ink(tall_i, Stroked::No), squeezed_ink(tall_i, Stroked::Appearance)); + let past = stroked - plain; + assert!((past - SQUEEZED_WEIGHT).abs() < 0.5, "the stroke reaches {past} pt past the squeezed stem, both sides together ({plain} → {stroked})"); +} + +/// A path's stroke still bends with the art: squeezed to half its width, it reaches half as far past +/// the path's left and right sides. +#[test] +fn a_paths_stroke_still_bends_in_a_squeezed_envelope() { + let (plain, stroked) = (squeezed_ink(rect, Stroked::No), squeezed_ink(rect, Stroked::Own)); + let past = stroked - plain; + assert!( + (past - SQUEEZED_WEIGHT / 2.0).abs() < 0.5, + "the stroke reaches {past} pt past the squeezed rectangle, both sides together ({plain} → {stroked})" + ); +} + +/// The type's stroke paints past the envelope's grid, where the glyphs reach its edge: the +/// envelope's visual bounds hold it (#6644). +#[test] +fn the_visual_bounds_of_an_envelope_hold_its_types_strokes() { + let (s, e, middle) = squeezed(tall_i, Stroked::Own); + let n = node(&s, e); + let (visual, grid) = (n.visual_bounds().unwrap(), n.geometric_bounds().unwrap()); + let (first, last) = ink_across(&s, middle); + assert!(first < grid.x0 - 1.0, "the stroke paints past the grid: {first} vs {grid:?}"); + // The ink is measured to whole pixels (an eighth of a point); miter spikes don't count, as in + // type's own visual bounds. + let pixel = 0.125; + assert!(visual.x0 <= first + pixel && visual.x1 >= last - pixel, "{visual:?} holds the ink from {first} to {last}"); + assert!(first - visual.x0 < 1.0 && visual.x1 - last < 1.0, "{visual:?} reaches no further than the ink from {first} to {last}"); +} + +/// A repeat of stroked type reaches past its copies by as far as the characters' strokes paint +/// past their glyphs: half the weight of a centred stroke, as type's own visual bounds (#6644). +#[test] +fn the_visual_bounds_of_a_repeat_of_stroked_type_grow_by_half_the_stroke() { + let mut s = session(); + let t = id_of(&s.execute("text.create", &json!({"x": 100, "y": 200, "text": "I", "size": 100})).unwrap()); + s.execute("paint.setStroke", &json!({"ids": [t.0], "color": "#ff0000"})).unwrap(); + s.execute("stroke.set", &json!({"ids": [t.0], "weight": SQUEEZED_WEIGHT})).unwrap(); + let r = id_of(&s.execute("object.repeat.grid", &json!({"ids": [t.0], "rows": 2, "cols": 2})).unwrap()); + let n = node(&s, r); + let (visual, geometric) = (n.visual_bounds().unwrap(), n.geometric_bounds().unwrap()); + let grown = [geometric.x0 - visual.x0, geometric.y0 - visual.y0, visual.x1 - geometric.x1, visual.y1 - geometric.y1]; + assert!(grown.iter().all(|g| (g - SQUEEZED_WEIGHT / 2.0).abs() < 1e-6), "{grown:?}"); +} + +/// An I stroked by `stroked` in a one-patch mesh envelope that leaves it as it is: the envelope. +fn enveloped_i(s: &mut Session, stroked: bool) -> NodeId { + let t = tall_i(s); + if stroked { + s.execute("paint.setStroke", &json!({"ids": [t.0], "color": "#ff0000"})).unwrap(); + s.execute("stroke.set", &json!({"ids": [t.0], "weight": SQUEEZED_WEIGHT})).unwrap(); + } + sel(s, &[t]); + id_of(&s.execute("object.envelope.makeWithMesh", &json!({"rows": 1, "cols": 1})).unwrap()) +} + +/// Type's stroke in an envelope inside an envelope also paints at its own weight: the outer +/// envelope squeezes the inner one's type to half its width, and the stem still has half the weight +/// of stroke on each side (#6644). +#[test] +fn type_strokes_paint_at_their_own_weight_in_an_envelope_inside_an_envelope() { + let (plain, stroked) = (squeezed_ink(|s| enveloped_i(s, false), Stroked::No), squeezed_ink(|s| enveloped_i(s, true), Stroked::No)); + let past = stroked - plain; + assert!((past - SQUEEZED_WEIGHT).abs() < 0.5, "the stroke reaches {past} pt past the squeezed stem, both sides together ({plain} → {stroked})"); +} + +/// An opacity mask on type bends as other art does (#6644): with Distort Appearance its stroke +/// becomes its bent outline, as on a path, while the type's own stroke stays a stroke. +#[test] +fn an_opacity_mask_on_type_bends_as_other_art() { + let mut s = session(); + let t = tall_i(&mut s); + s.execute("paint.setStroke", &json!({"ids": [t.0], "color": "#ff0000"})).unwrap(); + s.execute("stroke.set", &json!({"ids": [t.0], "weight": SQUEEZED_WEIGHT})).unwrap(); + let m = id_of(&s.execute("shape.rectangle", &json!({"x": 90, "y": 150, "width": 120, "height": 300})).unwrap()); + s.execute("paint.setStroke", &json!({"ids": [m.0], "color": "#ffffff"})).unwrap(); + s.execute("stroke.set", &json!({"ids": [m.0], "weight": SQUEEZED_WEIGHT})).unwrap(); + sel(&mut s, &[t, m]); + s.execute("transparency.makeOpacityMask", &json!({})).unwrap(); + assert!(node(&s, t).extras().mask.is_some(), "the type has the mask"); + sel(&mut s, &[t]); + let e = arch(&mut s, 60.0, json!({"distortAppearance": true})); + let (mut masks, mut kept) = (vec![], 0); + for n in &evaluated(&s, e) { + n.walk(&mut |c| { + kept += c.appearance.painted_strokes().count(); + masks.extend(c.extras().mask.as_ref().map(|m| m.art.clone())); + }); + } + assert_eq!(masks.len(), 1, "the mask goes with the type"); + let mut in_mask = 0; + masks[0].walk(&mut |c| in_mask += c.appearance.painted_strokes().count()); + assert_eq!(in_mask, 0, "the mask's stroke became its bent outline"); + assert_eq!(kept, 1, "the type's stroke stays a stroke"); +} + +/// Expand turns an envelope of stroked type into its bent glyph outlines with the stroke on them, +/// at its own weight, as it draws (#6644). +#[test] +fn expanding_an_envelope_of_stroked_type_keeps_the_stroke_at_its_weight() { + let (mut s, e, _) = squeezed(tall_i, Stroked::Own); + sel(&mut s, &[e]); + s.execute("object.envelope.expand", &json!({})).unwrap(); + let mut weights = vec![]; + node(&s, e).walk(&mut |c| weights.extend(c.appearance.painted_strokes().map(|st| st.width))); + assert_eq!(weights, [SQUEEZED_WEIGHT]); +} diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 45a7a92c8c..8f64280367 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5796,7 +5796,8 @@ fn a_warp_of_type_opens_as_a_mesh_on_its_saved_grid_and_keeps_its_shape_when_its /// A warp of stroked type with Distort Appearance whose drawn art maps the type's bounds with its /// strokes onto the grid the file saves (not its glyphs' bounds alone) opens live as a mesh envelope -/// mapping from that box (#3179). +/// mapping from that box (#3179). The drawn art strokes the bent outlines at the stroke's own weight, +/// as files draw it (#6644). #[test] fn a_warp_of_stroked_type_maps_from_its_bounds_with_strokes_where_its_drawn_art_does() { use vectorcraft_doc::live::{envelope_content_boxes, envelope_stroke_margin}; @@ -5816,7 +5817,7 @@ fn a_warp_of_stroked_type_maps_from_its_bounds_with_strokes_where_its_drawn_art_ vectorcraft_doc::NodeId(1_000_000), NodeKind::Envelope { content, kind: Box::new(rising_kind()), fidelity: 50.0, editing: false, options, frame: Box::default() }, ); - let (drawn, page) = drawn_envelope(&shaped); + let (drawn, page) = drawn_stroked_envelope(&shaped); let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope_on(&arc_keys(0.3), &rising_grid(), &drawn, ""))), &doc); let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); @@ -5833,6 +5834,101 @@ fn a_warp_of_stroked_type_maps_from_its_bounds_with_strokes_where_its_drawn_art_ assert_eq!(margin, Some(options.bounds_margin)); } +/// The drawn art of `envelope` (on the page axes), an envelope of type stroked by its characters, +/// as the drawn art of `.ai` files strokes type in an envelope: the bent glyph outlines the envelope +/// draws of its type unstroked, each filled in its type's color, then each stroked with the type's +/// stroke (its weight, color, join and miter limit) whatever the grid does to it, and the page's +/// PostScript for them (#6644). +fn drawn_stroked_envelope(envelope: &Node) -> (String, String) { + let NodeKind::Envelope { content, .. } = &envelope.kind else { panic!("an envelope") }; + let Some(NodeKind::Text(t)) = content.first().map(|c| &c.kind) else { panic!("type") }; + let stroke = t.runs[0].style.stroke_layer(); + let mut unstroked = envelope.clone(); + if let NodeKind::Envelope { content, .. } = &mut unstroked.kind { + for c in content.iter_mut() { + if let NodeKind::Text(t) = &mut std::sync::Arc::make_mut(c).kind { + t.runs.iter_mut().for_each(|r| r.style.stroke = vectorcraft_color::Paint::None); + } + } + } + let (fills, fill_page) = drawn_envelope(&unstroked); + let [r, g, b] = stroke.paint.color().map_or([0.0; 3], |c| c.to_rgb()); + let join = match stroke.join { + vectorcraft_doc::LineJoin::Miter => 0, + vectorcraft_doc::LineJoin::Round => 1, + vectorcraft_doc::LineJoin::Bevel => 2, + }; + let (w, limit) = (stroke.width, stroke.miter_limit); + let mut art = format!("{fills}0 0 0 0 {r} {g} {b} XA\n{w} w\n{join} j\n{limit} M\n"); + let mut page = format!("{fill_page}{r} {g} {b} setrgbcolor {w} setlinewidth {join} setlinejoin {limit} setmiterlimit\n"); + let pt = |p: kurbo::Point| { + let (x, y) = to_art(p); + format!("{x} {y}") + }; + vectorcraft_effects::expand_live_deep(None, &unstroked).walk(&mut |n| { + let NodeKind::Path { path, .. } = &n.kind else { return }; + for el in path.to_bezpath().elements() { + match *el { + kurbo::PathEl::MoveTo(p) => { + art += &format!("{} m\n", pt(p)); + page += &format!("newpath {} moveto ", pt(p)); + } + kurbo::PathEl::LineTo(p) => { + art += &format!("{} L\n", pt(p)); + page += &format!("{} lineto ", pt(p)); + } + kurbo::PathEl::CurveTo(a, c, p) => { + art += &format!("{} {} {} C\n", pt(a), pt(c), pt(p)); + page += &format!("{} {} {} curveto ", pt(a), pt(c), pt(p)); + } + kurbo::PathEl::QuadTo(..) => {} + kurbo::PathEl::ClosePath => { + art += "s\n"; + page += "closepath stroke\n"; + } + } + } + }); + (art, page) +} + +/// An envelope of type with a character stroke, Distort Appearance on, whose drawn art strokes the +/// bent glyph outlines at the stroke's own weight (as `.ai` files draw the strokes of type in an +/// envelope, however much the grid squeezes or stretches it) opens live: VectorCraft draws the +/// stroke that way too (#6644). +#[test] +fn an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live() { + let Some(family) = installed_family() else { return }; + // "FLUX" stroked 1.5 points wide in black with round joins. + let stroked = "/53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> /54 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 0.0 0.0 1.0 ] >> >> /57 true /62 1 /63 1.5"; + let doc = type_envelope_text(&family, None).replace("/53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >>", stroked); + let mut text = made_envelope_type(&doc); + text.visible = true; + assert!( + matches!(&text.kind, NodeKind::Text(t) if t.runs[0].style.stroke_width == 1.5 && t.runs[0].style.stroke_join == vectorcraft_doc::LineJoin::Round), + "{text:?}" + ); + let shaped = Node::new( + vectorcraft_doc::NodeId(1_000_000), + NodeKind::Envelope { + content: vec![std::sync::Arc::new(text)], + kind: Box::new(rising_kind()), + fidelity: 50.0, + editing: false, + options: vectorcraft_doc::live::EnvelopeOptions::NEW, + frame: Box::default(), + }, + ); + let (drawn, page) = drawn_stroked_envelope(&shaped); + let art = + native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope_on(&arc_keys(0.3), &rising_grid(), &drawn, ""))), &doc); + let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{:?}", r.warnings); + assert!(loose.is_empty(), "{loose:?}"); +} + /// A warp of type whose editing data saves no grid it can be shaped by keeps its drawn art and its /// type, hidden, with the note, when its own warp draws apart from that art (#3179). #[test] diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index d42e770df0..79785b2b00 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art, #6570; `.ai` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.ai` and EPS import, and what every export and `.ai` save draw: type's strokes in an envelope paint at their own weight along its bent outlines, Distort Appearance on or off, #6644; `.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art, #6570; `.ai` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -339,6 +339,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | `.ai` and EPS import, and what the canvas, every export and `.ai` save draw: type's strokes in an envelope (its characters' and those on its own appearance) paint along the bent glyph outlines at their own weight, Distort Appearance on or off, as the drawn art of users' `.ai` files strokes them (used locally, never committed: there the drawn art keeps those strokes as stroked paths at the type's own weights, and its ring is as wide wherever the grid squeezes or stretches the type). With Distort Appearance, VectorCraft bent them with the art, as their filled outlines, so an envelope of stroked type in a squeezed or stretched grid didn't draw as its drawn art and kept it. The same holds in an envelope inside an envelope. Paths' strokes, geometry effects, opacity masks, linear gradients and pattern fills bend as before. On 224 of our own files: envelopes of type that open live go from 135 in 37 files to 154 in 45, and no file loses one; no file changes whether it opens from its layers or its PDF part; renders at 40 ppi change in 31 files, by at most 2.3% of their pixels, 19 closer to the file's own page and 12 further, 11 of those where an envelope of type now opens live in place of its drawn art (the page's own art) and one by 4 pixels. Tests `envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope`, `a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope`, `a_paths_stroke_still_bends_in_a_squeezed_envelope`, `tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live` (#6644) | | 2026-11-11 | minor | `.ai` and EPS import, and what SVG export, PDF export and `.ai` save draw: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as a `.ai`'s own page aligns them (see type-parity.md, same date). In 224 users' files (used locally, never committed), 63 render differently: 54 closer to the page, 7 further, 1 alike and 1 that couldn't be compared; among them the median share of pixels that differ from the page went from 4.4% to 1.0%. Of the 7: one now opens from its layers instead of its PDF part (they differ from the page by less than the import's 5% limit now; type note 3.8%); one has a centred line whose spacing inside it differs from the page's, which now centres on the page's line while half of it no longer lies on the page's glyphs (type note 1.4% to 2.4%); five moved by less than 0.05 of a percentage point, in type the page draws in other glyphs. The type note shows a smaller share in 6 files; 2 more envelopes of type open live (129 to 131, none fewer in any file). SVG import: `text-anchor` aligns a line's advance, which ends after its last character's letter spacing, so a centred or right-aligned letter-spaced line moves back by half or all of it and stays where a reader draws it; SVG export, which anchors lines so, reads back unchanged (test `import_text::anchored_letter_spaced_lines_stay_where_a_reader_draws_them`). Affinity import: centred or right-aligned tracked artistic text moves by half or all of its last character's tracking, as layout now aligns it (unverified against Affinity, whose files we don't have), and differently aligned paragraphs line up by the widths layout aligns them by (`affinity_features::artistic_text_aligns_its_lines_within_the_longest`). Scores and estimates unchanged. (#6610) | | 2026-11-11 | minor | `.ai` and EPS import: while an envelope's contents are edited, the editing data keeps its mesh (a mesh's, a top object's or a warp's grid) in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art, as if the mesh hadn't followed the envelope when it moved: the envelope drew its contents elsewhere and kept its drawn art. A mesh or top object saved in Edit Contents (or a warp of type tried on its saved grid) whose mesh draws the contents as large as the drawn art, within half a point over width and height together, but further from it than that, is now tried again, after every other way and with one more of the import's checks, with its mesh moved by as far as the drawn art's box lies from the box of what it draws; it opens live there where it then draws as the drawn art. In users' files, used locally and never committed, only envelopes saved in Edit Contents draw elsewhere than their drawn art, and nothing in their dictionaries says by how much they moved. Tests `ai::tests::an_envelope_in_edit_contents_whose_mesh_stayed_behind_opens_live_on_its_drawn_art`, `tests_native::a_warp_of_type_in_edit_contents_whose_grid_stayed_behind_opens_on_its_drawn_art` (#6570) | | 2026-11-11 | minor | `.ai` save (PDF-compatible editing copy and legacy) and `.ai`/EPS open: a layer's `Lb` colour index follows the file's numbering, where Magenta, Cyan and Yellow are 4, 5 and 6 and `LAYER_COLORS` has Yellow, Magenta and Cyan; Yellow, Magenta and Cyan layers no longer come out as Magenta, Cyan and Yellow, and the other 24 presets are unchanged (#6502, from the index and RGB pairs in users' files). `LAYER_COLORS` keeps its order, so `.vectorcraft` files and the Layers panel don't change. Scores and estimates unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 5826c5c8c4..75f5dfe80c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7922,8 +7922,14 @@ laid-out box, and so does type inside any other object, such as a blend, a repea VectorCraft's own rule, and envelopes of type in saved files redraw by it. Everything inside bends with the envelope: images as a raster mesh warp (cut into pieces by Fidelity, each a clipped image under its own affine map; SVG shares the pixels between the pieces), symbol instances as their symbol's art, -and with Distort Appearance strokes (as their filled outlines) and geometry effects; with it, Distort Linear Gradients -bends linear gradients and Distort Pattern Fills the pattern tiles. A transformed envelope keeps its warp in its own +and with Distort Appearance the strokes of paths (as their filled outlines) and geometry effects; with it, Distort +Linear Gradients bends linear gradients and Distort Pattern Fills the pattern tiles. Type's strokes (its characters' +and those on its own appearance) don't bend, with Distort Appearance or without: they paint along the bent glyph +outlines at their own weight, as `.ai` files draw type in an envelope, so a squeezed or stretched envelope keeps them +as wide as they are, also in an envelope inside an envelope (#6644). Their geometry effects and an opacity mask on +the type still bend, and Expand gives the bent outlines with the strokes on them, which Object › Expand or Outline +Stroke outlines later. Before #6644, VectorCraft bent type's strokes too, so envelopes of stroked type made earlier +now draw their strokes at their own weight. A transformed envelope keeps its warp in its own frame (`kind.frame` in `document.inspect`; rotating a warp envelope turns the warp with it), and move and scale keep it square to the page. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f742..a5d874c65e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -1233,6 +1233,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Window › Blend exposes easing Type, Ramp and independent color Shift through `object.blend.options`. Smooth Color counts the steps of each pair of keys from their color change, or from the distance between their visible bounding-box edges when their colors match or they hold gradients or patterns ([Blend Options](https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html)); the step counts, their caps and the colors compared are VectorCraft's own. Envelope Options › Anti-Alias off draws an envelope's distorted images without smoothing, and Preserve Shape Using › Transparency gives each piece of a distorted image an opacity mask in its shape instead of a clipping mask ([Envelope panel options](http://web.archive.org/web/20260513013456/https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/envelope-panel-options.html)). + With Envelope Options › Distort Appearance on, type's strokes (its characters' and those on its own appearance) paint along the bent glyph outlines at their own weight instead of bending with the art, as the drawn art of users' `.ai` files strokes type in an envelope. This is VectorCraft's own rule from those files: the public page names applied effects and graphic styles, not strokes. This holds in an envelope inside an envelope too. Paths' strokes, geometry effects (type's included), opacity masks (on type too), linear gradients and pattern fills bend as before; Expand gives type's bent outlines with their strokes on them. The visual bounds of envelopes, blends and repeats, and the reach of symbol art, count type's character strokes as type's own visual bounds do (half the weight of a centred stroke); they left them out, though those strokes already painted past an envelope's grid. Envelopes of stroked type made in VectorCraft before this draw their strokes at their own weight now (#6644; `crates/engine/src/tests/envelope_distort.rs`, `crates/effects/src/bake/tests.rs`). The four polynomial distributions are original approximations of the public description, not measured numeric parity. See [Blend panel overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blend-panel-overview.html). A blend in a `.ai` or EPS opened from its editing data keeps a Specified Distance spacing with its distance ([Blend Options](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blend-options-overview.html)) where VectorCraft lays out as many steps between each pair of keys as the file draws, in the same places and colors. A Smooth Color blend opens live only where VectorCraft's own Smooth Color step count equals the drawn steps, and VectorCraft often counts a different number. A blend that VectorCraft doesn't lay out as drawn keeps its drawn steps with a note (`import::ai::tests::a_blend_keeps_its_spacing`, `a_blend_of_three_keys_keeps_its_distance_between_each_pair`, `a_blend_whose_spacing_doesnt_lay_out_its_steps_keeps_them`). @@ -1438,7 +1439,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). - In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and only where the layers with it are compared with the page and draw as it, the envelope weighed as the art it draws); elsewhere, and where the type's font isn't available, a text or visibility variable binds the type, or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). A warp of type whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the grid the file saves for it: its type stays editable and keeps the file's shape, but the warp's settings (style, bend) are lost, as the document has no place for them on a mesh. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its glyphs grown by as far as its strokes paint along a side (half the weight of a centred stroke), without what miter joins add at corners, where the size the file saves for it doesn't already give that box; the drawn-art check decides which one opens. An envelope's grid reads its way round from all its rows and columns, so a mesh bent far round reads the way it runs. While an envelope's contents are edited, its mesh is kept in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art (as if the mesh hadn't followed the envelope when it moved): a mesh or top object saved in Edit Contents whose mesh draws the contents as large as the drawn art but elsewhere is tried again, after every other way, with its mesh moved onto that art, and opens there where it then draws as it. + In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and only where the layers with it are compared with the page and draw as it, the envelope weighed as the art it draws); elsewhere, and where the type's font isn't available, a text or visibility variable binds the type, or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). A warp of type whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the grid the file saves for it: its type stays editable and keeps the file's shape, but the warp's settings (style, bend) are lost, as the document has no place for them on a mesh. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its glyphs grown by as far as its strokes paint along a side (half the weight of a centred stroke), without what miter joins add at corners, where the size the file saves for it doesn't already give that box; the drawn-art check decides which one opens. An envelope's grid reads its way round from all its rows and columns, so a mesh bent far round reads the way it runs. While an envelope's contents are edited, its mesh is kept in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art (as if the mesh hadn't followed the envelope when it moved): a mesh or top object saved in Edit Contents whose mesh draws the contents as large as the drawn art but elsewhere is tried again, after every other way, with its mesh moved onto that art, and opens there where it then draws as it. The drawn art of these files strokes type in an envelope along its bent outlines at the stroke's own weight, Distort Appearance on or off, and VectorCraft draws it so (#6644): an envelope of stroked type whose grid squeezes or stretches it opens live where it draws as that art. In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); a PDF 1.5 file with such a page is written as PDF 1.6 with PDF 1.5's encryption, PDF/X files and PDF 1.3 and 1.4 files write it in points (in PDF 1.3 and 1.4, a page past 32,767 pt that needs a real number past 32,767 is refused with an error naming it), a page whose short side is 3 pt or longer but would be under 3 units at that `UserUnit` gets a smaller one that keeps the short side at 3 units, and the export's warnings give each such page's size and how it is written (#6382). The Save PDF dialog's "Write pages longer than 200 in at full size in points" (`longPagesInPoints`, off by default) writes such pages in points without a `UserUnit` at every PDF version. @@ -1981,6 +1982,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | Envelopes: with Distort Appearance, type's strokes (characters' and appearance) paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them; paths' strokes, geometry effects, gradients and patterns bend as before; also in an envelope inside an envelope; opacity masks bend as other art; Expand keeps type's strokes as strokes; the visual bounds of envelopes, blends and repeats and the reach of symbol art count type's character strokes as type's own bounds do. Changes how VectorCraft's own envelopes of stroked type draw. Advanced art evidence and `.ai` import evidence updated (#6644); scores and estimates unchanged. | | 2026-11-11 | minor | Bundled content: built-in Energy Beams brush library (Laser Bolt, Plasma Arc, Charge Wave, Spark Trail); brush libraries evidence updated; ~44% → ~46%; more built-in brush libraries remain. | | 2026-11-11 | minor | Bundled content: built-in Skill Icons symbol library (Attack, Defend, Heal, Buff, Ult); symbol libraries evidence updated; ~42% → ~44%; more built-in symbol libraries remain. | | 2026-11-10 | minor | Automation: MCP tool count 33; headless `command_list` lists `UI_COMMANDS` (`ui: true`); eight prompts; effect-id vs command-id clarified (#6460); scores and hours unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index 702ecd5048..7d7af40c51 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (a centred or right-aligned line of tracked type is aligned without the tracking after its last character, as the page of users' `.ai` files aligns it, #6610; previously minor: tracking and manual kerning in horizontally scaled type are thousandths of the em as the Horizontal Scale draws it, as the page of users' `.ai` files spaces such type; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (type's strokes in an envelope paint along its bent glyph outlines at their own weight, Distort Appearance on or off, as users' `.ai` files draw them, #6644; previously minor: a centred or right-aligned line of tracked type is aligned without the tracking after its last character, as the page of users' `.ai` files aligns it, #6610; previously minor: tracking and manual kerning in horizontally scaled type are thousandths of the em as the Horizontal Scale draws it, as the page of users' `.ai` files spaces such type; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -81,6 +81,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-11 | minor | Type in envelopes: its strokes (its characters' and those on its own appearance) paint along the bent glyph outlines at their own weight, Distort Appearance on or off, as the drawn art of users' `.ai` files strokes them; with Distort Appearance they bent with the art as their filled outlines. The same holds in an envelope inside an envelope. Geometry effects on type's fills and strokes, and an opacity mask on type, still bend; Expand gives the bent outlines with the strokes on them. The visual bounds of envelopes, blends and repeats count type's character strokes as type's own visual bounds do. This changes how VectorCraft's own envelopes of stroked type draw (#6644). Score unchanged (estimated) | | 2026-11-11 | minor | Aligned lines of tracked type: a centred or right-aligned line is as wide as its characters without the tracking after its last one (`LineInfo::trailing`), so a centred line sits half that tracking right of where it did and a right-aligned line's last glyph ends on its anchor or frame edge, the tracking after it reaching past. The page of users' `.ai` files (used locally, never committed) aligns tracked lines so: matched glyph by glyph with the page, 176 centred lines of point type in 51 files sat half their last character's tracking left of it (median 0.500 of it; 143 within 0.05 pt), 58 right-aligned lines in 5 files the whole tracking (median 1.001), and 182 left-aligned lines where the page has them; in a line whose last character is tracked 150 and the others −34, the offset is half the last character's tracking (0.498 of it). All those lines end a paragraph. The last character is the last the line draws before a paragraph's end, a forced line break or the spaces a wrap leaves (they hang past the line). Spaces or a tab typed before a paragraph's end or a forced line break are the line's last characters, and the letter before them keeps its tracking: in the 2 such lines in the files, the page counts the space and the letter's tracking and leaves out only the space's own (VectorCraft still leaves trailing spaces out of the width). Forced line breaks and wrapped lines of tracked type aren't in the files: they take the rule unverified, so that Convert To Point Type and Artwork Cleanup's Break Text, which turn wraps into paragraphs and forced line breaks, keep the lines where they were. A hyphenated line, one whose last character hangs, justified, vertical and right-to-left lines and type on a path keep the tracking, and so do right, centre and decimal tab stops, which still align the text after them with its last character's tracking. Manual kerning on the last character, line breaking and justification are unchanged (none in the files). The tracking stays in the last glyph's advance (the caret after it and a selection of it take it in), and Fit Headline still counts it, as line breaking does. Right-aligned area type sets the text after its last tab the same way. Convert To Area Type makes the frame end where the lines are aligned, with room for that tracking, so tracked type converted either way stays where it is (it moved by the tracking on each round trip). Affinity import aligns artistic text by the same rule, so centred or right-aligned tracked artistic text moves by half or all of its last character's tracking (unverified against Affinity). Centred and right-aligned tracked type lays out differently than before in every document. Tests `tests::an_aligned_line_leaves_its_last_characters_tracking_out_of_its_width`, `tests::which_lines_leave_the_tracking_after_their_last_character_out`, `tests::an_aligned_line_leaves_out_its_last_tracking_as_glyph_scaling_sets_it`, `tests::the_text_after_the_last_tab_of_right_aligned_type_leaves_its_last_tracking_out`, and the engine's `converting_tracked_type_between_point_and_area_keeps_it_in_place` and `break_tracked_area_type_keeps_its_aligned_lines_in_place`. Scores and estimates unchanged. (#6610) | | 2026-11-11 | minor | Tracking and manual kerning in horizontally scaled type: they are thousandths of the em as the Horizontal Scale draws it (`vectorcraft_text::spacing_scale`), so at 50% a tracking of 100 adds 0.05 em after each character, half as much as at 100%. They were added at the full em whatever the scale, so narrow tracked type drifted further right with each letter. Illustrator's own layout in users' `.ai` files (used locally, not committed) spaces such type so, and PDF scales its character spacing by its horizontal scaling alike (ISO 32000-1 §9.4.4). Inline graphics take it after them too; vertical type keeps the full em, its advances down the column not all taking the Horizontal Scale. The automatic kerning the Character panel reads, Touch Type's limits and Artwork Cleanup's Combine Point Text measure in the same em, and Scale Text Attributes now keeps tracked type where a horizontal stretch of its transform drew it. Type that has both a Horizontal Scale other than 100% and tracking or manual kerning lays out differently than before in every document. Tests `tests::tracking_and_manual_kerning_take_the_horizontal_scale`, `tests::typo::optical_pairs_in_horizontally_scaled_type_measure_without_the_scaled_tracking`, `tests::inline::shift_tracking_and_missing_symbols`, `tests::vertical::vertical_tracking_keeps_the_em_at_any_horizontal_scale` and the engine's `horizontally_scaled_pair_reads_as_the_kerning_that_keeps_it`, `scale_text_attributes_keeps_tracked_type_where_it_was` and `combine_puts_horizontally_scaled_pieces_back_where_they_were`. Scores and estimates unchanged. | | 2026-11-10 | minor | Edit Custom Dictionary (#4936): Edit › Edit Custom Dictionary… adds, deletes and changes the custom dictionary's words, applied on Done; the Check Spelling dialog remains. Scores and estimates unchanged. |