From 41477041ea638dec807cb15015432f29737e9beb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 16:19:15 -0800 Subject: [PATCH] .ai save: the editing copy keeps artboard display marks and dense patterns Each artboard's DisplayMark was written as 0. It now holds the center mark (1), cross hairs (2) and video safe areas (4), as the reader reads them. A pattern fill that would place more tiles than a fill places came back with no tiles and was written as an empty group, so the shape was missing from the editing copy. Such a shape is now filled gray, as a pattern that isn't found is, with a warning. --- crates/eps/src/ai_editing.rs | 4 +++- crates/eps/src/ai_editing/tests.rs | 36 +++++++++++++++++++++++++++++ crates/eps/src/ai_editing/writer.rs | 8 +++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/ai_editing.rs b/crates/eps/src/ai_editing.rs index 7ca953703..bdcefb56b 100644 --- a/crates/eps/src/ai_editing.rs +++ b/crates/eps/src/ai_editing.rs @@ -366,7 +366,9 @@ fn artboards(s: &mut String, doc: &Document, to_art: Affine, origin: (f64, f64)) s.push_str("/RealPoint\n%_ (RulerOrigin) ,\n%_"); let par = ab.video_ruler_pixel_aspect_ratio; ps::push_num(s, if par.is_finite() && par > 0.0 { par } else { 1.0 }); - let _ = writeln!(s, " /Real (PAR) ,\n%_0 /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == 0)); + // Display marks: 1 the center mark, 2 cross hairs, 4 video safe areas. + let mark = u8::from(ab.show_center_mark) | u8::from(ab.show_cross_hairs) << 1 | u8::from(ab.show_video_safe_areas) << 2; + let _ = writeln!(s, " /Real (PAR) ,\n%_{mark} /Int (DisplayMark) ,\n%_{} /Bool (IsArtboardSelected) ,", u8::from(i == 0)); let _ = writeln!(s, "%_{} /UnicodeString (Name) ,\n%_; ,", ps::bytes(ab.name.as_bytes())); } s.push_str("%_; (ArtboardArray) ,\n"); diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index cb96fa9ff..5a355fe3d 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -949,6 +949,42 @@ fn a_group_fill_in_a_pattern_tile_is_written() { assert!(all.iter().any(|p| p.appearance.items.iter().any(|i| paint(i.paint()) == red)), "the tiles keep their group's fill"); } +/// A pattern fill that would place more tiles than a fill places is written as the shape filled +/// gray, as a pattern that isn't found is, instead of as nothing. +#[test] +fn a_pattern_fill_of_too_many_tiles_is_written_gray() { + let mut doc = Document::new(1200.0, 1200.0); + let layer = doc.layers[0].id; + let dot = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 0.5, 0.5)), Appearance::basic(solid(Color::BLACK), Paint::None, 0.0)); + let tiny = vectorcraft_doc::PatternDef { tile: Rect::new(0.0, 0.0, 1.0, 1.0), ..vectorcraft_doc::PatternDef::new("Tiny", vec![Arc::new(dot)]) }; + doc.patterns.push(tiny); + let filled = Node::path( + doc.alloc_id(), + shapes::rectangle(Rect::new(100.0, 100.0, 1100.0, 1100.0)), + Appearance::basic(Paint::Pattern { pattern: "Tiny".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 gray = paint(&solid(Color::gray(0.5))); + assert!(all.iter().any(|p| p.appearance.items.iter().any(|i| paint(i.paint()) == gray)), "{}", all.len()); + let out = export_ai_editing(&doc, &opts()).unwrap(); + assert!(out.warnings.iter().any(|w| w == writer::PATTERN_TILES), "{:?}", out.warnings); +} + +/// An artboard's center mark, cross hairs and video safe areas are written as the bits of its +/// `DisplayMark` and read back. +#[test] +fn artboard_display_marks_are_written() { + let mut doc = Document::new(200.0, 100.0); + (doc.artboards[0].show_center_mark, doc.artboards[0].show_video_safe_areas) = (true, true); + assert!(program(&doc).contains("%_5 /Int (DisplayMark) ,\n")); + let back = read_back(&doc); + let ab = &back.artboards[0]; + assert!(ab.show_center_mark && !ab.show_cross_hairs && ab.show_video_safe_areas); +} + #[test] fn effects_on_a_fill_are_reported_left_out() { let mut doc = Document::new(200.0, 100.0); diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index 325203188..e0afd0366 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -27,6 +27,7 @@ const MAX_AVERAGED_PIXELS: u64 = 1 << 24; pub(super) const TYPE: &str = "type is written as its outlines in the editing data for other apps"; pub(super) const GRADIENTS: &str = "gradients are written as their average color in the editing data for other apps"; const PATTERNS: &str = "pattern fills are written as their tiles cut to the shape in the editing data for other apps"; +pub(super) const PATTERN_TILES: &str = "pattern fills with more tiles than a fill places are written filled gray in the editing data for other apps"; pub(super) const CLIPPING: &str = "clipping groups and clipping layers are written as their art cut to the clipping path in the editing data for other apps"; pub(super) const IMAGES: &str = "images are written as their outlines filled with their average color in the editing data for other apps"; @@ -457,6 +458,13 @@ impl<'a> Writer<'a> { return self.path_object(path, rule, Some(&flat), None); }; let tiles = def.instances_in(*xf, path.to_bezpath().bounding_box()); + // More tiles than a fill places come back as none: the shape is filled gray, as a + // pattern that isn't found is. + if tiles.is_empty() && !def.art.is_empty() { + self.warn(PATTERN_TILES); + let flat = FillLayer { paint: Paint::solid(Color::gray(0.5)), ..f.clone() }; + return self.path_object(path, rule, Some(&flat), None); + } let outer = self.clip.clone(); let region = self.cut(path, rule); self.clip = Some(region);