diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index bff3ed60a..529c54a5f 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -2259,6 +2259,58 @@ fn gradient_definitions_skip_the_names_of_swatches_and_patterns() { same_lines(&gradient_looks(&read_back(&doc)), &gradient_looks(&doc)); } +/// Each search for a new gradient name starts after the last name given, so naming many gradients +/// tries each name once. +#[test] +fn gradient_names_are_searched_from_the_last_one_given() { + let mut taken: std::collections::HashSet = ["Gradient 2", "Gradient 5"].map(String::from).into(); + let (mut next, mut tries) = (1, 0); + let mut names = vec![]; + for _ in 0..1000 { + let name = writer::free_gradient_name(&mut next, |name| { + tries += 1; + taken.contains(name) + }); + taken.insert(name.clone()); + names.push(name); + } + assert_eq!(names[..4], ["Gradient", "Gradient 3", "Gradient 4", "Gradient 6"]); + assert_eq!(names.last().map(String::as_str), Some("Gradient 1002")); + assert_eq!(tries, 1002); + assert_eq!(next, 1003); +} + +/// Many gradients that no swatch names each get their own name, in order, skipping the names of +/// swatches and patterns, and the same names when the document is written again. +#[test] +fn many_gradient_definitions_take_distinct_names_in_order() { + let mut doc = one_object(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), vec![]); + let check = square(&mut doc, Rect::new(0.0, 0.0, 5.0, 5.0), Color::BLACK); + let mut taken = vectorcraft_doc::PatternDef::new("Gradient 7", vec![Arc::new(check)]); + taken.tile = Rect::new(0.0, 0.0, 10.0, 10.0); + doc.patterns.push(taken); + swatches_plus(&mut doc, vec![Swatch { name: "Gradient 3".into(), paint: solid(Color::rgb(0.0, 0.5, 0.0)), global: false, spot: false }]); + let layer = doc.layers[0].id; + for i in 0..300u16 { + let shade = f32::from(i) / 300.0; + let g = ramp(GradientKind::Linear, &[(0.0, Color::rgb(shade, 0.0, 0.0), 1.0, 0.5), (1.0, Color::rgb(0.0, 0.0, 1.0), 1.0, 0.5)]); + let x = f64::from(i % 20) * 10.0; + let y = f64::from(i / 20) * 6.0; + let n = painted( + &mut doc, + shapes::rectangle(Rect::new(x, y, x + 8.0, y + 5.0)), + vec![AppearanceItem::Fill(FillLayer::new(gradient_paint(g, None, None)))], + ); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + } + let text = program(&doc); + let names: Vec<&str> = text.lines().filter_map(|l| l.strip_prefix("%AI5_BeginGradient: ")).collect(); + let expected: Vec = + std::iter::once("(Gradient)".to_string()).chain((2..=302).filter(|&i| i != 3 && i != 7).map(|i| format!("(Gradient {i})"))).collect(); + assert_eq!(names, expected); + assert_eq!(text, program(&doc)); +} + /// A hard edge: a linear gradient whose stops all lie at one place is written from its last stop, /// and reads back with each stop on its side. #[test] diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index 21c9a7068..78602b939 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -22,7 +22,7 @@ pub(super) use images::PAYLOAD; #[cfg(test)] pub(super) use images::{IMAGE_DEPTH, IMAGES, LINKS_EMBEDDED}; #[cfg(test)] -pub(super) use paints::{GRADIENT_MIX, GRADIENT_SHAPES, GRADIENTS, STOP_LINKS, SWATCH_PLACEMENT}; +pub(super) use paints::{GRADIENT_MIX, GRADIENT_SHAPES, GRADIENTS, STOP_LINKS, SWATCH_PLACEMENT, free_gradient_name}; /// How deep symbols, live objects and pattern tiles may nest in one another (deeper art is left /// out). Groups and layers need no limit of their own: a document is never deeper than @@ -141,6 +141,8 @@ pub(super) struct Writer<'a> { canvas: Affine, /// The gradient definitions, in the order they are first used. gradients: Vec, + /// The number the next new gradient definition's name tries first (1 is `Gradient`). + next_gradient: u64, /// The pattern definitions written, each after those its art uses. patterns: Vec, /// The patterns whose definitions are being written. @@ -167,6 +169,7 @@ impl<'a> Writer<'a> { tile_grad_space: Affine::translate((-origin.0, origin.1)), canvas: Affine::new([1.0, 0.0, 0.0, -1.0, origin.0, origin.1]) * to_art, gradients: vec![], + next_gradient: 1, patterns: vec![], defining: vec![], in_palette: false, diff --git a/crates/eps/src/ai_editing/writer/paints.rs b/crates/eps/src/ai_editing/writer/paints.rs index 09af15e18..d805d9d0e 100644 --- a/crates/eps/src/ai_editing/writer/paints.rs +++ b/crates/eps/src/ai_editing/writer/paints.rs @@ -49,6 +49,18 @@ fn writable_gradient(g: &Gradient) -> bool { g.kind != GradientKind::Freeform && !g.stops.is_empty() && g.stops.len() <= crate::import::MAX_GRADIENT_STOPS } +/// The first name of `Gradient`, `Gradient 2`, `Gradient 3`, … numbered `next` or higher for +/// which `taken` returns false (`Gradient` is number 1); `next` is set past it. +pub(in crate::ai_editing) fn free_gradient_name(next: &mut u64, mut taken: impl FnMut(&str) -> bool) -> String { + loop { + let name = if *next <= 1 { String::from("Gradient") } else { format!("Gradient {next}") }; + *next = next.saturating_add(1); + if !taken(&name) { + return name; + } + } +} + /// The stops of `g` in order of their place on the ramp. fn sorted_stops(g: &Gradient) -> Vec<&GradientStop> { let mut stops: Vec<&GradientStop> = g.stops.iter().collect(); @@ -72,7 +84,8 @@ impl Writer<'_> { /// The name of the definition of `g`: the swatch `swatch` names when it is a gradient swatch /// of the same gradient, else a definition of the same gradient that no swatch names, else a - /// new one (`Gradient`, numbered when taken by another definition, a swatch or a pattern). + /// new one ([`free_gradient_name`]: `Gradient`, numbered when taken by another definition, a + /// swatch or a pattern). fn define_gradient(&mut self, g: &Gradient, swatch: Option<&str>) -> String { let used = !self.in_palette; let found = swatch @@ -82,16 +95,10 @@ impl Writer<'_> { d.used |= used; return d.name.clone(); } - let doc = self.doc; - let taken = |gradients: &[GradientDef], name: &str| { + let (doc, gradients) = (self.doc, &self.gradients); + let name = free_gradient_name(&mut self.next_gradient, |name| { gradients.iter().any(|d| d.name == name) || doc.swatch(name).is_some() || doc.pattern(name).is_some() - }; - let mut name = String::from("Gradient"); - let mut i = 1; - while taken(&self.gradients, &name) { - i += 1; - name = format!("Gradient {i}"); - } + }); self.gradients.push(GradientDef { name: name.clone(), gradient: g.clone(), swatch: false, used }); name }