From 80c43c8133b524f351778b68fffdd4deee1b7610 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 21:52:04 -0700 Subject: [PATCH] Save As .ai: keep the group fills and strokes of pattern tiles in the editing data A pattern fill is written as its tiles cut to the shape. A group in the tile art with fills or strokes of its own was written as its members alone, without that paint, because only the layers and the symbols' art were baked before writing. The patterns' tile art is now baked the same way as the symbols' art, so its group paint and geometry effects are written as plain paths. --- crates/eps/src/ai_editing.rs | 27 ++++++++++++++++++--------- crates/eps/src/ai_editing/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/eps/src/ai_editing.rs b/crates/eps/src/ai_editing.rs index 552a99fe0..d87a00396 100644 --- a/crates/eps/src/ai_editing.rs +++ b/crates/eps/src/ai_editing.rs @@ -156,26 +156,35 @@ fn crlf(s: &str) -> Vec { s.replace('\n', "\r\n").into_bytes() } -/// `doc` with its geometry effects baked ([`vectorcraft_effects::bake_document`]), in its symbols' -/// art too (which instances are written as); `None` when nothing changes. +/// `doc` with its geometry effects and group paint baked ([`vectorcraft_effects::bake_document`]), +/// in its symbols' art and its patterns' tile art too (which instances and pattern fills are +/// written as); `None` when nothing changes. fn baked(doc: &Document) -> Option { let baked = vectorcraft_effects::bake_document(doc); let d = baked.as_ref().unwrap_or(doc); - if !d.symbols.iter().any(|s| vectorcraft_effects::needs_bake(&s.art)) { + let needs = vectorcraft_effects::needs_bake; + if !d.symbols.iter().any(|s| needs(&s.art)) && !d.patterns.iter().any(|p| p.art.iter().any(|a| needs(a))) { return baked; } - // The symbols' art baked as the layers of a copy of the document. + // The symbols' art, then each pattern's art, baked as the layers of a copy of the document. let mut art = d.clone(); - art.layers = d.symbols.iter().map(|s| Arc::new(Node::group(s.art.id, vec![s.art.clone()]))).collect(); + let symbols = d.symbols.iter().map(|s| Arc::new(Node::group(s.art.id, vec![s.art.clone()]))); + let patterns = d.patterns.iter().map(|p| Arc::new(Node::group(vectorcraft_doc::NodeId(0), p.art.clone()))); + art.layers = symbols.chain(patterns).collect(); // The copy keeps what baking adds to the document (the images color adjustments make). let Some(mut out) = vectorcraft_effects::bake_document(&art) else { return baked }; - let arts: Vec>> = out.layers.iter().map(|l| l.children().and_then(|c| c.first()).cloned()).collect(); - out.layers = d.layers.clone(); - for (s, a) in out.symbols.iter_mut().zip(arts) { - if let Some(a) = a { + let layers = std::mem::replace(&mut out.layers, d.layers.clone()); + let mut members = layers.iter().map(|l| l.children().cloned().unwrap_or_default()); + for s in out.symbols.iter_mut() { + if let Some(a) = members.next().and_then(|m| m.first().cloned()) { s.art = a; } } + for p in out.patterns.iter_mut() { + if let Some(m) = members.next() { + p.art = m; + } + } Some(out) } diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index e7f9c9d39..696014fb9 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -718,6 +718,27 @@ fn an_adjusted_image_in_a_symbol_is_written_in_its_adjusted_color() { assert_ne!(fill, paint(&solid(Color::rgb(0.0, 0.0, 0.0))), "the image is adjusted"); } +#[test] +fn a_group_fill_in_a_pattern_tile_is_written() { + let mut doc = Document::new(200.0, 100.0); + let layer = doc.layers[0].id; + let dot = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default()); + let mut group = Node::group(doc.alloc_id(), vec![Arc::new(dot)]); + group.appearance = Appearance::basic(solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into(); + doc.patterns.push(vectorcraft_doc::PatternDef::new("Dots", vec![Arc::new(group)])); + let filled = Node::path( + doc.alloc_id(), + shapes::rectangle(Rect::new(20.0, 20.0, 40.0, 40.0)), + Appearance::basic(Paint::Pattern { pattern: "Dots".into(), xf: Affine::IDENTITY }, Paint::None, 0.0), + ); + doc.insert(Some(layer), usize::MAX, filled).unwrap(); + let back = read_back(&doc); + let mut all = vec![]; + paths(back.layers[0].children().unwrap(), &mut all); + let red = paint(&solid(Color::rgb(1.0, 0.0, 0.0))); + assert!(all.iter().any(|p| p.appearance.items.iter().any(|i| paint(i.paint()) == red)), "the tiles keep their group's fill"); +} + #[test] fn effects_on_a_fill_are_reported_left_out() { let mut doc = Document::new(200.0, 100.0);