mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
.ai import: stroked type in an envelope maps from its glyphs grown by half its stroke
With Distort Appearance, the stroked candidate for type in an envelope mapped from the type's bounds with strokes, which for a stroke on the type's appearance (or on a group round it) reach as far as a miter join may at a corner, the miter limit times half the weight. In users' files the drawn art maps from the glyphs grown by what the strokes paint along a side: half the weight of a centred stroke. The candidate now uses that box. Character strokes keep the measure the type's bounds with strokes already give them (they have no miter allowance), and other objects in the contents keep their bounds with strokes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8b6c7444be
commit
6701a1f8e5
@@ -131,15 +131,48 @@ 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.
|
||||
/// The box of `content` an envelope of type with Distort Appearance maps from in a file: type by
|
||||
/// its glyph outlines 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, as are groups by their own strokes;
|
||||
/// other objects by their bounds with strokes. `None` when nothing in it has bounds.
|
||||
fn half_stroked_box(content: &[Arc<Node>]) -> Option<Rect> {
|
||||
/// How far the visible strokes of `appearance` paint along the sides of closed outlines.
|
||||
fn along(appearance: &Appearance) -> f64 {
|
||||
appearance.painted_strokes().map(|s| s.side_reach(true)).fold(0.0, f64::max)
|
||||
}
|
||||
/// `r` grown by `by` on each side, where that is a distance.
|
||||
fn grown(r: Rect, by: f64) -> Rect {
|
||||
if by.is_finite() && by > 0.0 { r.inflate(by, by) } else { r }
|
||||
}
|
||||
fn walk(n: &Arc<Node>, acc: Option<Rect>) -> Option<Rect> {
|
||||
let one = std::slice::from_ref(n);
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
let Some((_, ink)) = envelope_content_boxes(one, Affine::IDENTITY) else { return acc };
|
||||
// The characters' own strokes as the type's bounds with strokes measure them (they
|
||||
// have no miter allowance there), and the strokes of its appearance along a side.
|
||||
vectorcraft_geom::union_opt(acc, Some(grown(ink, t.char_stroke_outset().max(along(&n.appearance)))))
|
||||
}
|
||||
NodeKind::Group { children, clip: false } => {
|
||||
let members = children.iter().fold(None, |acc, c| walk(c, acc));
|
||||
vectorcraft_geom::union_opt(acc, members.map(|b| grown(b, along(&n.appearance))))
|
||||
}
|
||||
_ => vectorcraft_geom::union_opt(acc, envelope_content_boxes(one, Affine::IDENTITY).map(|(stroked, _)| stroked)),
|
||||
}
|
||||
}
|
||||
content.iter().fold(None, |acc, c| walk(c, acc))
|
||||
}
|
||||
|
||||
/// Envelope `kind` mapping from its contents' glyphs grown by as far as their strokes paint along a
|
||||
/// side ([`half_stroked_box`]), 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)?;
|
||||
let (_, plain) = envelope_content_boxes(content, Affine::IDENTITY)?;
|
||||
let margin = envelope_stroke_margin(half_stroked_box(content)?, plain)?;
|
||||
if margin == options.bounds_margin || margin.iter().any(|m| *m < 0.0) || margin.iter().all(|m| *m == 0.0) {
|
||||
return None;
|
||||
}
|
||||
@@ -518,7 +551,8 @@ impl WarpedType {
|
||||
/// 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
|
||||
/// is also checked mapping from its type's glyphs grown by half their strokes' weight
|
||||
/// ([`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.
|
||||
@@ -581,9 +615,9 @@ impl WarpedType {
|
||||
// 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.
|
||||
// With Distort Appearance, a file's envelope may map its type from its glyphs grown by half
|
||||
// their strokes' weight or from the glyphs alone, 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)
|
||||
@@ -772,3 +806,91 @@ fn alike_within(live: &Node, drawn: &Node, reach: Option<Rect>, layer: NodeId, s
|
||||
*spent = Spent { any, solid };
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, CharStyle, LineJoin, StrokeLayer, TextObject};
|
||||
|
||||
/// An envelope of `content` with Distort Appearance, on a flat one-patch mesh.
|
||||
fn envelope_of(content: Vec<Arc<Node>>) -> NodeKind {
|
||||
let points = vec![Point::new(0.0, 0.0), Point::new(100.0, 0.0), Point::new(0.0, 50.0), Point::new(100.0, 50.0)];
|
||||
NodeKind::Envelope {
|
||||
content,
|
||||
kind: Box::new(EnvelopeKind::Mesh { rows: 1, cols: 1, points, handles: vec![] }),
|
||||
fidelity: 50.0,
|
||||
editing: false,
|
||||
options: EnvelopeOptions::NEW,
|
||||
frame: Box::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The bounds margin of the stroked candidate for an envelope of `content`.
|
||||
fn stroked_margin(content: Vec<Arc<Node>>) -> Option<[f32; 4]> {
|
||||
match with_strokes(&envelope_of(content)) {
|
||||
Some(NodeKind::Envelope { options, .. }) => Some(options.bounds_margin),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// "AVA" at 40 points, at (10, 60).
|
||||
fn type_node(id: u64, style: CharStyle) -> Node {
|
||||
Node::new(NodeId(id), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 60.0), "AVA", CharStyle { size: 40.0, ..style }))))
|
||||
}
|
||||
|
||||
/// A 4 point stroke with miter joins at a limit of 4.
|
||||
fn mitred() -> StrokeLayer {
|
||||
let mut stroke = StrokeLayer::new(Paint::solid(Color::BLACK), 4.0);
|
||||
(stroke.join, stroke.miter_limit) = (LineJoin::Miter, 4.0);
|
||||
stroke
|
||||
}
|
||||
|
||||
/// Type with Distort Appearance and a stroke from its appearance is tried mapped from its glyphs
|
||||
/// grown by half the stroke's weight, not from its bounds with strokes, which reach as far as a
|
||||
/// miter join may at a corner.
|
||||
#[test]
|
||||
fn stroked_type_is_tried_mapped_from_its_glyphs_grown_by_half_its_stroke() {
|
||||
let mut text = type_node(1, CharStyle::default());
|
||||
let mut appearance = (*text.appearance).clone();
|
||||
appearance.items.push(AppearanceItem::Stroke(mitred()));
|
||||
text.appearance = appearance.into();
|
||||
let content = vec![Arc::new(text)];
|
||||
let (with, glyphs) = envelope_content_boxes(&content, Affine::IDENTITY).unwrap();
|
||||
let half = glyphs.inflate(2.0, 2.0);
|
||||
assert!(with.width() > half.width() + 1.0, "{with:?} {half:?}");
|
||||
assert_eq!(stroked_margin(content), envelope_stroke_margin(half, glyphs));
|
||||
}
|
||||
|
||||
/// Type stroked by its characters alone is tried mapped from its bounds with strokes, as before,
|
||||
/// turned and stretched as well as upright.
|
||||
#[test]
|
||||
fn type_with_a_character_stroke_is_tried_mapped_from_its_bounds_with_strokes() {
|
||||
let style = CharStyle { stroke: Paint::solid(Color::BLACK), stroke_width: 4.0, ..CharStyle::default() };
|
||||
for xf in [
|
||||
Affine::IDENTITY,
|
||||
Affine::rotate(std::f64::consts::FRAC_PI_4),
|
||||
Affine::scale_non_uniform(std::f64::consts::SQRT_2, std::f64::consts::FRAC_1_SQRT_2),
|
||||
] {
|
||||
let mut text = type_node(1, style.clone());
|
||||
if let NodeKind::Text(t) = &mut text.kind {
|
||||
t.xf = xf * t.xf;
|
||||
}
|
||||
let content = vec![Arc::new(text)];
|
||||
let (with, glyphs) = envelope_content_boxes(&content, Affine::IDENTITY).unwrap();
|
||||
assert_eq!(stroked_margin(content), envelope_stroke_margin(with, glyphs), "{xf:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke on a group's own appearance round type grows the box by half its weight too.
|
||||
#[test]
|
||||
fn a_stroke_on_a_group_of_type_grows_the_box_by_half_its_weight() {
|
||||
let mut group = Node::new(NodeId(2), NodeKind::Group { children: vec![Arc::new(type_node(1, CharStyle::default()))], clip: false });
|
||||
let mut appearance = (*group.appearance).clone();
|
||||
appearance.items.push(AppearanceItem::Stroke(mitred()));
|
||||
group.appearance = appearance.into();
|
||||
let content = vec![Arc::new(group)];
|
||||
let (_, glyphs) = envelope_content_boxes(&content, Affine::IDENTITY).unwrap();
|
||||
assert_eq!(stroked_margin(content), envelope_stroke_margin(glyphs.inflate(2.0, 2.0), glyphs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.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: stroked type in an envelope with Distort Appearance is 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, for a stroke on its appearance or on a group round it as well as its characters'; it was mapped from its bounds with strokes, which reach as far as a miter join may at a corner. Tests `ai::envelope::tests::stroked_type_is_tried_mapped_from_its_glyphs_grown_by_half_its_stroke`, `type_with_a_character_stroke_is_tried_mapped_from_its_bounds_with_strokes`, `a_stroke_on_a_group_of_type_grows_the_box_by_half_its_weight` (part of #3179) |
|
||||
| 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` |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user