diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index a3396a251..9bd3e937a 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -529,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); @@ -541,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, + waiting: usize, ) -> Result<(NodeId, Vec), TypeKept> { let TypedEnvelope { obj, mut before, mut inner, drawn } = envelope; let art = doc.node(drawn).cloned().ok_or(TypeKept::Read)?; @@ -595,8 +600,11 @@ impl WarpedType { (plain.visible, plain.opacity) = (true, 1.0); let page = self.page.filter(|_| shown.contains(&drawn)); let mut alike = None; - for kind in kinds { - if self.checks == 0 { + 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; diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index e8ad5a3f5..35ccdb5bd 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5826,6 +5826,82 @@ fn a_warp_of_type_without_a_saved_grid_keeps_its_drawn_art() { 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 { let NodeKind::Text(t) = &n.kind else { return None };