mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 12:24:54 +00:00
Merge pull request #6508 from jmverges/fix/3179-warps-of-type-as-mesh
.ai import: warps of type open as mesh envelopes on their saved grid
This commit is contained in:
@@ -20,8 +20,9 @@
|
||||
//! `(AIDeformArtKey)`) and the kind (`AIDeformStyle`: 1 a warp, 2 a mesh, 3 a top object). Then the
|
||||
//! distorted art on plain lines, and the plugin's `XP`. While the contents are edited, the group
|
||||
//! holds the contents and the dictionary the mesh. A top object's outline is the edge of its
|
||||
//! one-patch mesh. An envelope whose parts don't read, or that doesn't come out as drawn, keeps its
|
||||
//! drawn art.
|
||||
//! one-patch mesh. A warp's mesh is a grid of its shape: a warp of type whose own warp doesn't come
|
||||
//! out as drawn opens as a mesh envelope on that grid where that does. An envelope whose parts don't
|
||||
//! read, or that doesn't come out as drawn, keeps its drawn art.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::sync::Arc;
|
||||
@@ -130,6 +131,25 @@ fn saved_margin(o: &Obj, content: &[Arc<Node>]) -> Option<[f32; 4]> {
|
||||
envelope_stroke_margin(stroked, plain)
|
||||
}
|
||||
|
||||
/// Envelope `kind` mapping from its contents' bounds with their strokes, for an envelope with
|
||||
/// Distort Appearance whose contents have strokes it doesn't map from already; `None` otherwise.
|
||||
fn with_strokes(kind: &NodeKind) -> Option<NodeKind> {
|
||||
let NodeKind::Envelope { content, options, .. } = kind else { return None };
|
||||
if !options.distort_appearance {
|
||||
return None;
|
||||
}
|
||||
let (stroked, plain) = envelope_content_boxes(content, Affine::IDENTITY)?;
|
||||
let margin = envelope_stroke_margin(stroked, plain)?;
|
||||
if margin == options.bounds_margin || margin.iter().any(|m| *m < 0.0) || margin.iter().all(|m| *m == 0.0) {
|
||||
return None;
|
||||
}
|
||||
let mut kind = kind.clone();
|
||||
if let NodeKind::Envelope { options, .. } = &mut kind {
|
||||
options.bounds_margin = margin;
|
||||
}
|
||||
Some(kind)
|
||||
}
|
||||
|
||||
/// A warp: its style, bend, distortions and orientation.
|
||||
fn warp(o: &Obj) -> Option<EnvelopeKind> {
|
||||
let style = whole(o, "DeformStyle")??;
|
||||
@@ -188,9 +208,38 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec<GridPoint>)> {
|
||||
let mut pts: Vec<GridPoint> = m.points.iter().map(|q| (q.p, q.handles)).collect();
|
||||
let at = |pts: &[GridPoint], cols: u32, r: u32, c: u32| pts.get((r * (cols + 1) + c) as usize).map_or(Point::ZERO, |q| q.0);
|
||||
let remap = |h: [Vec2; 4], slots: [usize; 4]| slots.map(|s| h.get(s).copied().unwrap_or(Vec2::ZERO));
|
||||
// Columns that run down the page: the grid goes the other way round.
|
||||
let along = at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0);
|
||||
if along.y.abs() > along.x.abs() {
|
||||
// How far the grid reaches: a run much shorter than that is rounding, not a direction.
|
||||
let reach = {
|
||||
let first = pts.first().map_or(Point::ZERO, |q| q.0);
|
||||
let bounds = pts.iter().fold(Rect::from_points(first, first), |b, q| b.union_pt(q.0));
|
||||
bounds.width().hypot(bounds.height()) * 1e-6
|
||||
};
|
||||
// How the grid runs along its rows (every row end to end, added up) and down its columns (every
|
||||
// column): read from all of them, a grid bent far round (its end columns leaning more than
|
||||
// halfway over) still reads the way it runs. Each sum comes with how long its parts are: where
|
||||
// they nearly cancel out, as in a ring, what is left is rounding, not a direction, and the grid
|
||||
// reads by its first row or column instead.
|
||||
let runs = |pts: &[GridPoint], rows: u32, cols: u32| {
|
||||
let (mut along, mut down, mut along_len, mut down_len) = (Vec2::ZERO, Vec2::ZERO, 0.0, 0.0);
|
||||
for r in 0..=rows {
|
||||
let v = at(pts, cols, r, cols) - at(pts, cols, r, 0);
|
||||
(along, along_len) = (along + v, along_len + v.hypot());
|
||||
}
|
||||
for c in 0..=cols {
|
||||
let v = at(pts, cols, rows, c) - at(pts, cols, 0, c);
|
||||
(down, down_len) = (down + v, down_len + v.hypot());
|
||||
}
|
||||
let clear = |v: Vec2, len: f64| (v.hypot() >= len * 0.01 && v.hypot() > reach).then_some(v);
|
||||
let first_row = at(pts, cols, 0, cols) - at(pts, cols, 0, 0);
|
||||
let first_col = at(pts, cols, rows, 0) - at(pts, cols, 0, 0);
|
||||
(clear(along, along_len).unwrap_or(first_row), clear(down, down_len).unwrap_or(first_col))
|
||||
};
|
||||
// How much of a run is across the page.
|
||||
let level = |v: Vec2| if v.hypot() > 0.0 { v.x.abs() / v.hypot() } else { 0.0 };
|
||||
// Columns that run across the page more than rows do: the grid goes the other way round. Nearly
|
||||
// as much (a grid turned an eighth round) keeps the file's way round.
|
||||
let (along, down) = runs(&pts, rows, cols);
|
||||
if level(down) > level(along) + 1e-3 {
|
||||
let mut t = Vec::with_capacity(len);
|
||||
for r in 0..=cols {
|
||||
for c in 0..=rows {
|
||||
@@ -204,7 +253,7 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec<GridPoint>)> {
|
||||
(rows, cols) = (cols, rows);
|
||||
pts = t;
|
||||
}
|
||||
if (at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0)).x < 0.0 {
|
||||
if runs(&pts, rows, cols).0.x < 0.0 {
|
||||
for row in pts.chunks_mut(cols as usize + 1) {
|
||||
row.reverse();
|
||||
for (_, h) in row.iter_mut() {
|
||||
@@ -212,7 +261,7 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec<GridPoint>)> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (at(&pts, cols, rows, 0) - at(&pts, cols, 0, 0)).y < 0.0 {
|
||||
if runs(&pts, rows, cols).1.y < 0.0 {
|
||||
let mut flipped: Vec<GridPoint> = pts.chunks(cols as usize + 1).rev().flatten().copied().collect();
|
||||
for (_, h) in &mut flipped {
|
||||
h.swap(H_DOWN, H_UP);
|
||||
@@ -269,6 +318,23 @@ fn unwrapped(mut nodes: Vec<Node>) -> Vec<Arc<Node>> {
|
||||
/// The live envelope of dictionary `o`, the group on `%_` lines before it (`before`) and the art
|
||||
/// in its `AIDeformArtKey` block (`inner`); `None` where they don't make one.
|
||||
pub(super) fn live(o: &Obj, before: Option<Node>, inner: Vec<Node>) -> Option<NodeKind> {
|
||||
envelope_as(o, before, inner, false)
|
||||
}
|
||||
|
||||
/// Is the envelope of dictionary `o` a warp?
|
||||
fn is_warp(o: &Obj) -> bool {
|
||||
whole(o, "AIDeformStyle") == Some(Some(1))
|
||||
}
|
||||
|
||||
/// [`live`] for a warp, as a mesh envelope on the grid its editing data saves (the mesh that shapes
|
||||
/// it), its points and handles as saved: it draws as that grid does, without the warp's style, bend
|
||||
/// and distortions. `None` for an envelope that isn't a warp, or whose grid doesn't read.
|
||||
fn live_on_saved_grid(o: &Obj, before: Option<Node>, inner: Vec<Node>) -> Option<NodeKind> {
|
||||
envelope_as(o, before, inner, true)
|
||||
}
|
||||
|
||||
/// [`live`], or with `on_grid` [`live_on_saved_grid`].
|
||||
fn envelope_as(o: &Obj, before: Option<Node>, inner: Vec<Node>, on_grid: bool) -> Option<NodeKind> {
|
||||
let editing = switch(o, "AIDeformEditModeKey")?;
|
||||
let (mut options, fidelity) = options(o)?;
|
||||
let before = before?;
|
||||
@@ -290,14 +356,14 @@ pub(super) fn live(o: &Obj, before: Option<Node>, inner: Vec<Node>) -> Option<No
|
||||
if options.distort_appearance {
|
||||
options.bounds_margin = saved_margin(o, &content).unwrap_or_default();
|
||||
}
|
||||
let kind = match whole(o, "AIDeformStyle")?? {
|
||||
1 => warp(o)?,
|
||||
2 => {
|
||||
let kind = match (whole(o, "AIDeformStyle")??, on_grid) {
|
||||
(1, false) => warp(o)?,
|
||||
(1, true) | (2, false) => {
|
||||
let (rows, cols, pts) = upright(shape?)?;
|
||||
let (points, handles) = pts.into_iter().unzip();
|
||||
EnvelopeKind::Mesh { rows, cols, points, handles }
|
||||
}
|
||||
3 => {
|
||||
(3, false) => {
|
||||
let (rows, cols, pts) = upright(shape?)?;
|
||||
// The outline runs clockwise from the top left corner, one segment per grid edge.
|
||||
let corners = [0, cols, cols.checked_add(rows)?, cols.checked_mul(2)?.checked_add(rows)?];
|
||||
@@ -448,9 +514,13 @@ impl WarpedType {
|
||||
/// Make each envelope of type live where it draws as its drawn art, its contents' text slots
|
||||
/// holding their type: `type_of` gives, for a slot's id, the hidden text object it became in
|
||||
/// `doc` and the type the contents take in its place (the slot's visibility and lock are the
|
||||
/// type's). The live envelope takes its drawn art's place and the hidden type goes. Each check
|
||||
/// uses one of the import's checks and, for an envelope that shows, adds to what the page check
|
||||
/// allows ([`drawn_alike`]). Elsewhere, and where the type is set in a font that isn't
|
||||
/// type's). The live envelope takes its drawn art's place and the hidden type goes. A warp that
|
||||
/// doesn't draw as its drawn art is checked again as a mesh envelope on the grid the file saves
|
||||
/// for it ([`live_on_saved_grid`]), which opens in its place where it does: its type stays
|
||||
/// editable in that shape, and the warp's style and bend are lost. With Distort Appearance, each
|
||||
/// is also checked mapping from its contents' bounds with their strokes ([`with_strokes`]). Each
|
||||
/// check uses one of the import's checks and, for an envelope that shows, adds to what the page
|
||||
/// check allows ([`drawn_alike`]). Elsewhere, and where the type is set in a font that isn't
|
||||
/// available, the drawn art and the hidden type stay.
|
||||
pub(crate) fn settle_envelopes(&mut self, doc: &mut Document, type_of: impl Fn(&Document, NodeId) -> Option<(NodeId, Node)>) -> TypedSettled {
|
||||
let mut settled = TypedSettled::default();
|
||||
@@ -459,8 +529,11 @@ impl WarpedType {
|
||||
}
|
||||
let mut shown = BTreeSet::new();
|
||||
doc.layers.iter().for_each(|l| collect_shown(l, &mut shown));
|
||||
for envelope in std::mem::take(&mut self.envelopes) {
|
||||
match self.typed_live(doc, envelope, &type_of, &shown) {
|
||||
let envelopes = std::mem::take(&mut self.envelopes);
|
||||
let mut waiting = envelopes.len();
|
||||
for envelope in envelopes {
|
||||
waiting -= 1;
|
||||
match self.typed_live(doc, envelope, &type_of, &shown, waiting) {
|
||||
Ok(put_back) => {
|
||||
settled.live += 1;
|
||||
settled.put_back.push(put_back);
|
||||
@@ -471,14 +544,16 @@ impl WarpedType {
|
||||
settled
|
||||
}
|
||||
|
||||
/// [`Self::settle_envelopes`] for `envelope`, the shown objects of `doc` being `shown`: the live
|
||||
/// envelope's id and what takes its place again, or why its drawn art stays.
|
||||
/// [`Self::settle_envelopes`] for `envelope`, the shown objects of `doc` being `shown` and
|
||||
/// `waiting` envelopes of type still to come: the live envelope's id and what takes its place
|
||||
/// again, or why its drawn art stays.
|
||||
fn typed_live(
|
||||
&mut self,
|
||||
doc: &mut Document,
|
||||
envelope: TypedEnvelope,
|
||||
type_of: &impl Fn(&Document, NodeId) -> Option<(NodeId, Node)>,
|
||||
shown: &BTreeSet<NodeId>,
|
||||
waiting: usize,
|
||||
) -> Result<(NodeId, Vec<Node>), TypeKept> {
|
||||
let TypedEnvelope { obj, mut before, mut inner, drawn } = envelope;
|
||||
let art = doc.node(drawn).cloned().ok_or(TypeKept::Read)?;
|
||||
@@ -503,22 +578,47 @@ impl WarpedType {
|
||||
for n in before.iter_mut().chain(inner.iter_mut()) {
|
||||
put_type(n, &texts);
|
||||
}
|
||||
let kind = live(&obj, before, inner).ok_or(TypeKept::Read)?;
|
||||
if self.checks == 0 {
|
||||
return Err(TypeKept::Budget);
|
||||
// A warp that VectorCraft's own warp doesn't draw as its drawn art may still draw as the
|
||||
// grid the file saves for it: then it opens as a mesh envelope on that grid.
|
||||
let on_grid = if is_warp(&obj) { live_on_saved_grid(&obj, before.clone(), inner.clone()) } else { None };
|
||||
// With Distort Appearance, a file's envelope may map its contents from their bounds with
|
||||
// their strokes or without them, and for type the size the file saves for that box is often
|
||||
// out of date (not the size of the type there now): each is checked.
|
||||
let kinds: Vec<NodeKind> = live(&obj, before, inner)
|
||||
.into_iter()
|
||||
.chain(on_grid)
|
||||
.flat_map(|k| {
|
||||
let stroked = with_strokes(&k);
|
||||
std::iter::once(k).chain(stroked)
|
||||
})
|
||||
.collect();
|
||||
if kinds.is_empty() {
|
||||
return Err(TypeKept::Read);
|
||||
}
|
||||
self.checks -= 1;
|
||||
let mut envelope = art.clone();
|
||||
(envelope.id, envelope.kind) = (doc.alloc_id(), kind);
|
||||
let layer = doc.alloc_id();
|
||||
let shared = Shared { patterns: &doc.patterns, images: &doc.images };
|
||||
let (mut live_one, mut plain) = (envelope.clone(), art.clone());
|
||||
(live_one.visible, live_one.opacity) = (true, 1.0);
|
||||
let (id, layer) = (doc.alloc_id(), doc.alloc_id());
|
||||
let mut plain = art.clone();
|
||||
(plain.visible, plain.opacity) = (true, 1.0);
|
||||
let page = self.page.filter(|_| shown.contains(&drawn));
|
||||
if !drawn_alike(&live_one, &plain, layer, &shared, page, &mut self.spent) {
|
||||
return Err(TypeKept::Differs);
|
||||
let mut alike = None;
|
||||
for (i, kind) in kinds.into_iter().enumerate() {
|
||||
// A second try is only made while more checks are left than there are envelopes of
|
||||
// type and warped text objects still to come (some of which may need none): one
|
||||
// envelope's tries don't use up the checks of the others.
|
||||
if self.checks == 0 || (i > 0 && self.checks <= waiting + self.looks.len()) {
|
||||
return Err(TypeKept::Budget);
|
||||
}
|
||||
self.checks -= 1;
|
||||
let mut envelope = art.clone();
|
||||
(envelope.id, envelope.kind) = (id, kind);
|
||||
let shared = Shared { patterns: &doc.patterns, images: &doc.images };
|
||||
let mut live_one = envelope.clone();
|
||||
(live_one.visible, live_one.opacity) = (true, 1.0);
|
||||
if drawn_alike(&live_one, &plain, layer, &shared, page, &mut self.spent) {
|
||||
alike = Some(envelope);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let envelope = alike.ok_or(TypeKept::Differs)?;
|
||||
if doc.node(drawn).is_none() {
|
||||
return Err(TypeKept::Read);
|
||||
}
|
||||
|
||||
@@ -7501,6 +7501,101 @@ fn a_mesh_envelope_opens_live_with_its_points_and_handles() {
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A mesh envelope bent far round, its end columns leaning more than halfway over (a warp's grid
|
||||
/// bent into an arc), opens live the way it runs: its columns along the arc, as in the file.
|
||||
#[test]
|
||||
fn a_mesh_envelope_bent_far_round_opens_the_way_it_runs() {
|
||||
use vectorcraft_doc::live::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP};
|
||||
// 3 columns along an arc round (100, 0) from 160 to 20 degrees, rows at radius 60 and 90: the
|
||||
// end columns lean 70 degrees over.
|
||||
let at = |(i, j): (usize, usize)| {
|
||||
let (a, r) = ((160.0 - i as f64 * 140.0 / 3.0).to_radians(), 60.0 + j as f64 * 30.0);
|
||||
(100.0 + r * a.cos(), r * a.sin())
|
||||
};
|
||||
let handle = |p: (usize, usize), q: (usize, usize)| {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
};
|
||||
// Written with its first index across the arc and its second along it, so that the grid reads
|
||||
// turned round and has to be turned upright.
|
||||
let shape = mesh_text(1, 3, &|(i, j)| at((j, i)), &|p, q| handle((p.1, p.0), (q.1, q.0)), None);
|
||||
// Row r of the envelope is row 1 - r of the file (art space has y up).
|
||||
let doc_pt = |(x, y): (f64, f64)| Point::new(x, 100.0 - y);
|
||||
let (mut points, mut handles) = (vec![], vec![]);
|
||||
for r in 0..=1usize {
|
||||
for c in 0..=3usize {
|
||||
let g = (c, 1 - r);
|
||||
let p = doc_pt(at(g));
|
||||
points.push(p);
|
||||
let towards = |q: Option<(usize, usize)>| q.filter(|q| q.0 <= 3 && q.1 <= 1).map_or(kurbo::Vec2::ZERO, |q| doc_pt(handle(g, q)) - p);
|
||||
let mut h = [kurbo::Vec2::ZERO; 4];
|
||||
h[H_RIGHT] = towards(Some((g.0 + 1, g.1)));
|
||||
h[H_LEFT] = towards(g.0.checked_sub(1).map(|i| (i, g.1)));
|
||||
h[H_UP] = towards(Some((g.0, g.1 + 1)));
|
||||
h[H_DOWN] = towards(g.1.checked_sub(1).map(|j| (g.0, j)));
|
||||
handles.push(h);
|
||||
}
|
||||
}
|
||||
let want = EnvelopeKind::Mesh { rows: 1, cols: 3, points, handles };
|
||||
let r = read_ok(&layer(&envelope_of(&shape, ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&want))));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let EnvelopeKind::Mesh { rows, cols, points: got, .. } = live_envelope(&a[0]).1 else { panic!("{a:#?}") };
|
||||
assert_eq!((*rows, *cols), (1, 3));
|
||||
let EnvelopeKind::Mesh { points: want, .. } = &want else { panic!("{want:?}") };
|
||||
assert!(got.iter().zip(want).all(|(p, q)| (*p - *q).hypot() < 1e-6), "{got:?}");
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A mesh envelope bent halfway round, its grid running down an arc from top to bottom so that its
|
||||
/// end rows cancel out, opens the same way round wherever it sits on the page and whichever way
|
||||
/// round the file writes it.
|
||||
#[test]
|
||||
fn a_mesh_envelope_bent_halfway_round_opens_the_same_way_wherever_it_is() {
|
||||
use vectorcraft_doc::live::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP};
|
||||
for cx in [100.0, 100.1, 37.0, 250.3, 12.7] {
|
||||
// 4 rows down an arc round (cx, 0) from 90 to 270 degrees, columns at radius 90 and 60.
|
||||
let at = |(i, j): (usize, usize)| {
|
||||
let (a, r) = ((90.0 + i as f64 * 45.0_f64).to_radians(), 60.0 + j as f64 * 30.0);
|
||||
(cx + r * a.cos(), r * a.sin())
|
||||
};
|
||||
let handle = |p: (usize, usize), q: (usize, usize)| {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
};
|
||||
// Row r of the envelope is the arc's point r from the top, column c the radius 1 - c: the
|
||||
// outer one is on the left.
|
||||
let doc_pt = |(x, y): (f64, f64)| Point::new(x, 100.0 - y);
|
||||
let (mut points, mut handles) = (vec![], vec![]);
|
||||
for r in 0..=4usize {
|
||||
for c in 0..=1usize {
|
||||
let g = (r, 1 - c);
|
||||
let p = doc_pt(at(g));
|
||||
points.push(p);
|
||||
let towards = |q: Option<(usize, usize)>| q.filter(|q| q.0 <= 4 && q.1 <= 1).map_or(kurbo::Vec2::ZERO, |q| doc_pt(handle(g, q)) - p);
|
||||
let mut h = [kurbo::Vec2::ZERO; 4];
|
||||
h[H_DOWN] = towards(Some((g.0 + 1, g.1)));
|
||||
h[H_UP] = towards(g.0.checked_sub(1).map(|i| (i, g.1)));
|
||||
h[H_RIGHT] = towards(g.1.checked_sub(1).map(|j| (g.0, j)));
|
||||
h[H_LEFT] = towards(Some((g.0, g.1 + 1)));
|
||||
handles.push(h);
|
||||
}
|
||||
}
|
||||
let want = EnvelopeKind::Mesh { rows: 4, cols: 1, points, handles };
|
||||
let along = mesh_text(4, 1, &at, &handle, None);
|
||||
let across = mesh_text(1, 4, &|(i, j)| at((j, i)), &|p, q| handle((p.1, p.0), (q.1, q.0)), None);
|
||||
for shape in [along, across] {
|
||||
let r = read_ok(&layer(&envelope_of(&shape, ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&want))));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let EnvelopeKind::Mesh { rows, cols, points: got, .. } = live_envelope(&a[0]).1 else { panic!("at {cx}: {a:#?}") };
|
||||
assert_eq!((*rows, *cols), (4, 1), "at {cx}");
|
||||
let EnvelopeKind::Mesh { points: want, .. } = &want else { panic!("{want:?}") };
|
||||
assert!(got.iter().zip(want).all(|(p, q)| (*p - *q).hypot() < 1e-6), "at {cx}: {got:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope made with a top object opens as a live envelope whose top path is the edge of its
|
||||
/// one-patch mesh, an anchor on the edge included; its contents stay the rectangle.
|
||||
#[test]
|
||||
|
||||
@@ -5346,7 +5346,6 @@ fn type_envelope(keys: &str, drawn: &str) -> String {
|
||||
/// [`type_envelope`], with `after` (on plain lines, written on `%_` ones) after the text object in
|
||||
/// its contents, such as the text object's XML id.
|
||||
fn type_envelope_with(keys: &str, drawn: &str, after: &str) -> String {
|
||||
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
|
||||
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
let mesh = format!(
|
||||
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
|
||||
@@ -5355,9 +5354,16 @@ fn type_envelope_with(keys: &str, drawn: &str, after: &str) -> String {
|
||||
corner(80.0, 70.0, 80.0, 60.0, 70.0, 70.0),
|
||||
corner(80.0, 40.0, 70.0, 40.0, 80.0, 50.0),
|
||||
);
|
||||
type_envelope_on(keys, &mesh, drawn, after)
|
||||
}
|
||||
|
||||
/// [`type_envelope_with`], with `shape` (on plain lines, written on `%_` ones) in the group before
|
||||
/// its dictionary, where the editing data saves the envelope's mesh.
|
||||
fn type_envelope_on(keys: &str, shape: &str, drawn: &str, after: &str) -> String {
|
||||
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
|
||||
format!(
|
||||
"{}{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_1 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n",
|
||||
hidden(&format!("u\n{mesh}U\n")),
|
||||
hidden(&format!("u\n{shape}U\n")),
|
||||
hidden(&format!("/ArtDictionary :\n{keys}")),
|
||||
hidden(&format!("u\n{}{after}U\n9 () XW\n", text_object(0))),
|
||||
)
|
||||
@@ -5654,6 +5660,248 @@ fn an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art() {
|
||||
assert!(!r.warnings.iter().any(|n| n.contains("variable bindings")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A one-patch warp grid as the editing data saves it, which no warp style draws: its top edge
|
||||
/// rises to the right and bulges up. In art space, corners (20, 40) (20, 73) (80, 61) (80, 40), each
|
||||
/// with its handles towards the next corner and the previous one.
|
||||
fn rising_grid() -> String {
|
||||
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
format!(
|
||||
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
|
||||
corner(20.0, 40.0, 20.0, 51.0, 40.0, 40.0),
|
||||
corner(20.0, 73.0, 40.0, 80.0, 20.0, 62.0),
|
||||
corner(80.0, 61.0, 80.0, 54.0, 60.0, 72.0),
|
||||
corner(80.0, 40.0, 60.0, 40.0, 80.0, 47.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// [`rising_grid`] on the page axes as a mesh envelope reads it: its points from the top left, row
|
||||
/// by row, and their handles (towards the next column, the previous one, the next row and the
|
||||
/// previous one; none out of the grid).
|
||||
fn rising_kind() -> vectorcraft_doc::live::EnvelopeKind {
|
||||
use kurbo::{Point, Vec2};
|
||||
let z = Vec2::ZERO;
|
||||
vectorcraft_doc::live::EnvelopeKind::Mesh {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
points: vec![Point::new(20.0, 27.0), Point::new(80.0, 39.0), Point::new(20.0, 60.0), Point::new(80.0, 60.0)],
|
||||
handles: vec![
|
||||
[Vec2::new(20.0, -7.0), z, Vec2::new(0.0, 11.0), z],
|
||||
[z, Vec2::new(-20.0, -11.0), Vec2::new(0.0, 7.0), z],
|
||||
[Vec2::new(20.0, 0.0), z, z, Vec2::new(0.0, -11.0)],
|
||||
[z, Vec2::new(-20.0, 0.0), z, Vec2::new(0.0, -7.0)],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// The anchors of the paths `n` draws once its live art is expanded.
|
||||
fn drawn_anchors(n: &Node) -> Vec<kurbo::Point> {
|
||||
fn walk(n: &Node, out: &mut Vec<kurbo::Point>) {
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, .. } => out.extend(path.subpaths.iter().flat_map(|s| s.anchors.iter().map(|a| a.p))),
|
||||
_ => n.children().into_iter().flatten().for_each(|c| walk(c, out)),
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
walk(&vectorcraft_effects::expand_live_deep(None, n), &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// A warp of type whose own warp draws apart from its drawn art, where the grid the file saves for
|
||||
/// it draws as that art, opens as a mesh envelope on that grid, its points and handles as saved:
|
||||
/// the type is editable, and new text fills the same shape (#3179).
|
||||
#[test]
|
||||
fn a_warp_of_type_opens_as_a_mesh_on_its_saved_grid_and_keeps_its_shape_when_its_text_changes() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let Some(family) = installed_family() else { return };
|
||||
let doc = type_envelope_text(&family, None);
|
||||
let mut text = made_envelope_type(&doc);
|
||||
text.visible = true;
|
||||
let options = vectorcraft_doc::live::EnvelopeOptions::NEW;
|
||||
let shaped = |text: Node| {
|
||||
let content = vec![std::sync::Arc::new(text)];
|
||||
let kind = Box::new(rising_kind());
|
||||
Node::new(
|
||||
vectorcraft_doc::NodeId(1_000_000),
|
||||
NodeKind::Envelope { content, kind, fidelity: 50.0, editing: false, options, frame: Box::default() },
|
||||
)
|
||||
};
|
||||
let (drawn, page) = drawn_envelope(&shaped(text));
|
||||
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:?}");
|
||||
assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings);
|
||||
let mut live = None;
|
||||
r.document.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Envelope { .. }) {
|
||||
live = Some(n.clone());
|
||||
}
|
||||
});
|
||||
let mut live = live.unwrap();
|
||||
let NodeKind::Envelope { kind, content, .. } = &mut live.kind else { panic!("not an envelope: {live:?}") };
|
||||
let (EnvelopeKind::Mesh { rows, cols, points, handles }, EnvelopeKind::Mesh { points: want, handles: want_h, .. }) = (&**kind, rising_kind())
|
||||
else {
|
||||
panic!("not a mesh envelope: {kind:?}")
|
||||
};
|
||||
assert_eq!((*rows, *cols), (1, 1));
|
||||
assert!(points.iter().zip(&want).all(|(p, q)| (*p - *q).hypot() < 1e-6), "{points:?}");
|
||||
assert!(handles.iter().flatten().zip(want_h.iter().flatten()).all(|(h, g)| (*h - *g).hypot() < 1e-6), "{handles:?}");
|
||||
// New text in the envelope fills the grid as the old did: "HIH", whose corners are its glyphs',
|
||||
// reaches each corner of the grid.
|
||||
let NodeKind::Text(t) = &mut std::sync::Arc::make_mut(&mut content[0]).kind else { panic!("type") };
|
||||
let style = t.runs[0].style.clone();
|
||||
t.runs = vec![vectorcraft_doc::TextRun::new("HIH", style)];
|
||||
(t.cached_bounds, t.cached_ink) = (None, None);
|
||||
t.cached_baselines.clear();
|
||||
let anchors = drawn_anchors(&live);
|
||||
for corner in want {
|
||||
let near = anchors.iter().map(|a| (*a - corner).hypot()).fold(f64::INFINITY, f64::min);
|
||||
assert!(near < 0.25, "{corner:?} is {near} from the new text");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[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};
|
||||
let Some(family) = installed_family() else { return };
|
||||
// "FLUX" stroked 4 points wide in black.
|
||||
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 /63 4.0";
|
||||
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 == 4.0), "{text:?}");
|
||||
let content = vec![std::sync::Arc::new(text)];
|
||||
let (with, without) = envelope_content_boxes(&content, kurbo::Affine::IDENTITY).unwrap();
|
||||
let mut options = vectorcraft_doc::live::EnvelopeOptions::NEW;
|
||||
options.bounds_margin = envelope_stroke_margin(with, without).unwrap();
|
||||
assert!(options.bounds_margin.iter().all(|m| *m > 0.0), "{options:?}");
|
||||
let shaped = Node::new(
|
||||
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 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:?}");
|
||||
let mut margin = None;
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Envelope { options, .. } = &n.kind {
|
||||
margin = Some(options.bounds_margin);
|
||||
}
|
||||
});
|
||||
assert_eq!(margin, Some(options.bounds_margin));
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn a_warp_of_type_without_a_saved_grid_keeps_its_drawn_art() {
|
||||
let Some(family) = installed_family() else { return };
|
||||
let doc = type_envelope_text(&family, None);
|
||||
let mut text = made_envelope_type(&doc);
|
||||
text.visible = true;
|
||||
let content = vec![std::sync::Arc::new(text)];
|
||||
let options = vectorcraft_doc::live::EnvelopeOptions::NEW;
|
||||
let shaped = Node::new(
|
||||
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 art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope_on(&arc_keys(0.3), "", &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!(envelopes.is_empty(), "{envelopes:?}");
|
||||
assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings);
|
||||
assert_eq!(super::tests::program_warnings(&r.warnings), [crate::import::ai::ENVELOPE_TYPE_DRAWN], "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A warp of type tried again on its saved grid leaves a check for each envelope of type still to
|
||||
/// come: with one check too few for both, the next envelope, whose first try opens it, gets it.
|
||||
#[test]
|
||||
fn a_warp_of_type_tried_again_leaves_the_next_envelope_its_check() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let Some(family) = installed_family() else { return };
|
||||
let story = ("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0);
|
||||
let mut doc = text_document(&[story, story]).replace("(Helvetica)", &format!("({family})"));
|
||||
// [`native_with_text`] writes 60 characters a line: the end of the data, `~>`, isn't split.
|
||||
while ascii85(doc.as_bytes()).len() % 60 == 1 {
|
||||
doc.push('\n');
|
||||
}
|
||||
let text = made_envelope_type(&doc);
|
||||
// The first envelope, of story 0, draws as its saved grid and not as its warp: two checks.
|
||||
let mut shown = text.clone();
|
||||
shown.visible = true;
|
||||
let options = vectorcraft_doc::live::EnvelopeOptions::NEW;
|
||||
let shaped = Node::new(
|
||||
vectorcraft_doc::NodeId(1_000_000),
|
||||
NodeKind::Envelope {
|
||||
content: vec![std::sync::Arc::new(shown)],
|
||||
kind: Box::new(rising_kind()),
|
||||
fidelity: 50.0,
|
||||
editing: false,
|
||||
options,
|
||||
frame: Box::default(),
|
||||
},
|
||||
);
|
||||
let (drawn_grid, page_grid) = drawn_envelope(&shaped);
|
||||
let grid = type_envelope_on(&arc_keys(0.3), &rising_grid(), &drawn_grid, "");
|
||||
// The second, of story 1, draws as its warp: one check.
|
||||
let (drawn_arc, page_arc) = drawn_envelope(&arc_envelope(&text, 30.0));
|
||||
let arc = type_envelope(&arc_keys(0.3), &drawn_arc).replace("%_0 /StoryIndex", "%_1 /StoryIndex");
|
||||
// Envelopes of art before them use all but `left` of the import's checks.
|
||||
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
|
||||
let rect = "20 5 m\n20 95 L\n80 95 L\n80 5 L\n20 5 L\n";
|
||||
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
let mesh = format!(
|
||||
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
|
||||
corner(20.0, 5.0, 20.0, 35.0, 40.0, 5.0),
|
||||
corner(20.0, 95.0, 40.0, 95.0, 20.0, 65.0),
|
||||
corner(80.0, 95.0, 80.0, 65.0, 60.0, 95.0),
|
||||
corner(80.0, 5.0, 60.0, 5.0, 80.0, 35.0),
|
||||
);
|
||||
let red = "0 1 1 0 1 0 0 Xa\n";
|
||||
let filler = format!(
|
||||
"{}%_/ArtDictionary :\n%_131073 /Int (AIDeformOptionsKey) ,\n%_50 /Int (AIDeformFidelityKey) ,\n%_0 /Bool (AIDeformEditModeKey) ,\n%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n%_\nu\n{red}{rect}f\nU\n(Adobe Deform Plugin) 1 0 0 XP\n",
|
||||
hidden(&format!("u\n{mesh}U\n")),
|
||||
hidden(&format!("u\n{red}{rect}f\nU\n")),
|
||||
);
|
||||
let page =
|
||||
format!("{}\n1 0 0 setrgbcolor 20 5 moveto 20 95 lineto 80 95 lineto 80 5 lineto closepath fill\n{page_grid}{page_arc}", page_square(10, 10));
|
||||
let file = |left: usize| {
|
||||
let body = format!("{}\n{}{grid}{arc}", square(10, 10), filler.repeat(crate::import::ai::MAX_CHECKED - left));
|
||||
eps(&page, &native_with_text(&layer("Art", true, &body), &doc))
|
||||
};
|
||||
// The live envelopes of type, by kind.
|
||||
let typed = |d: &Document| {
|
||||
let mut out = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Envelope { content, kind, .. } = &n.kind
|
||||
&& content.iter().any(|c| matches!(c.kind, NodeKind::Text(_)))
|
||||
{
|
||||
out.push(if matches!(**kind, EnvelopeKind::Mesh { .. }) { "mesh" } else { "warp" });
|
||||
}
|
||||
});
|
||||
out
|
||||
};
|
||||
let r = import(&file(3)).unwrap();
|
||||
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
|
||||
assert_eq!(typed(&r.document), ["mesh", "warp"], "{:?}", r.warnings);
|
||||
let r = import(&file(2)).unwrap();
|
||||
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
|
||||
assert_eq!(typed(&r.document), ["warp"], "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// The fill of the first run of type `n`.
|
||||
fn text_fill(n: &Node) -> Option<vectorcraft_color::Color> {
|
||||
let NodeKind::Text(t) = &n.kind else { return None };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.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: 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: a warp of type (an envelope made with a warp around type) whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the warp grid the file saves, with the same drawn-art check, budget and page comparison as #6421: where it passes, the type is editable and keeps the file's shape, and the warp's settings (style, bend) are lost. A second try is only made while more checks are left than there are envelopes of type and warped text objects still to come. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its box with strokes, where the size the file saves for it doesn't already give that box (a rule chosen by measuring real files). An envelope's grid (warp, mesh or top object) now reads its way round from all its rows and columns, not its first row alone: a mesh bent far round (its end columns leaning more than halfway over) reads the way it runs, not turned a quarter round, and a grid symmetric about its middle reads the same wherever it is on the page. Tests `tests_native::a_warp_of_type_opens_as_a_mesh_on_its_saved_grid_and_keeps_its_shape_when_its_text_changes`, `a_warp_of_stroked_type_maps_from_its_bounds_with_strokes_where_its_drawn_art_does`, `a_warp_of_type_without_a_saved_grid_keeps_its_drawn_art`, `a_warp_of_type_tried_again_leaves_the_next_envelope_its_check`, `ai::tests::a_mesh_envelope_bent_far_round_opens_the_way_it_runs`, `a_mesh_envelope_bent_halfway_round_opens_the_same_way_wherever_it_is` (part of #3179) |
|
||||
| 2026-11-11 | minor | `.ai` and EPS import: type stroked in the text document keeps its stroke's cap, join and miter limit (keys 61, 62 and 64 of its character attributes, numbered as PostScript's `setlinecap` and `setlinejoin`: 0 butt or miter, 1 round, 2 projecting or bevel). They were left at a miter join with a limit of 10 whatever the file said, so type the file strokes with round joins, or with the usual limit of 4 or less, drew spikes at its sharp corners, some many points long. In users' files, used locally and never committed, the page strokes the type with the cap, join and miter limit these keys give; no file had a bevel join, which is read by the same numbering. A value that isn't one of theirs keeps the default, a miter limit past 500 is 500, and type without a stroke keeps the defaults. Dashes are not read: no file had dashed type to tell which keys hold them. Tests `ate::tests::a_run_keeps_its_stroke_cap_join_and_miter_limit`, `tests_native::stroked_type_keeps_its_cap_join_and_miter_limit`; scores unchanged |
|
||||
| 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, which weighs the envelope as the art it draws (not as type laid out again, which the comparison allows more for), and when they don't draw as it the drawn art and the hidden type are put back. Layers used though they don't draw as the page (the editing data trusted, or the objects that differ coming in as the page's art) leave envelopes of type their drawn art and hidden type too. A `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text or a visibility variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone`, `an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live`, `trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art`, `layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art`, `an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art` |
|
||||
| 2026-11-10 | minor | `.ai` save: a variable font's record in the text document's font set names the type's place in the font's design space (`/4`, each `fvar` axis as 16.16 fixed: the named instance's setting, or the run's own Variable Font setting) and the font's version (`/5`). Without them, type in a named instance such as Bold opened at the font's default instance in the app that opens the file. Two settings of one font are now two fonts of the set; a static font's record is unchanged. The record's form comes from the public report in #6388. Test `ai_editing::tests::variable_font_instances_name_their_design_space` (#6388) |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user