.ai import: a warp of type's second try leaves checks for the envelopes after it

A warp of type may now spend up to four checks (its warp and its saved
grid, each with and without strokes). A try after the first is only
made while more checks are left than there are envelopes of type and
warped text objects still to come, so one envelope's retries don't use
up the checks of the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jmverges
2026-11-11 03:06:24 +00:00
co-authored by Claude Opus 5.5
parent 198bb6dffd
commit d0c99cba4b
2 changed files with 90 additions and 6 deletions
+14 -6
View File
@@ -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<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)?;
@@ -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;
+76
View File
@@ -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<vectorcraft_color::Color> {
let NodeKind::Text(t) = &n.kind else { return None };