From 37e3c0215ac0b037da902259e3f94243aa2152e2 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 19:31:45 +0000 Subject: [PATCH] .ai import: type in an envelope may map from its bounds with strokes With Distort Appearance, an envelope's contents map from the box the file measured for them: their bounds with their strokes or without them. For type, the size the file saves for that box is often out of date, so it can't say which. Envelopes of type are now checked both ways: as before, and mapping from their bounds with their strokes. Whichever draws as the drawn art opens live. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai/envelope.rs | 40 +++++++++++++++++++++++---- crates/eps/src/import/tests_native.rs | 39 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index 3a45bf3a1..461be66fc 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -131,6 +131,25 @@ fn saved_margin(o: &Obj, content: &[Arc]) -> 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 { + 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 { let style = whole(o, "DeformStyle")??; @@ -479,10 +498,11 @@ impl WarpedType { /// 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. 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. + /// 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(); if self.envelopes.is_empty() { @@ -537,7 +557,17 @@ 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 }; - let kinds: Vec = live(&obj, before, inner).into_iter().chain(on_grid).collect(); + // 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 = 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); } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index e1b4f68e8..e8ad5a3f5 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5763,6 +5763,45 @@ 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). +#[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]