Merge pull request #5786 from dotCooCoo/fix/ai-gradient-name-serial

.ai save: new gradient names continue from the last one given
This commit is contained in:
BFlat
2026-11-05 18:10:50 +07:00
committed by GitHub
3 changed files with 73 additions and 11 deletions
+52
View File
@@ -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<String> = ["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<String> =
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]
+4 -1
View File
@@ -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<paints::GradientDef>,
/// 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<paints::PatternOut>,
/// 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,
+17 -10
View File
@@ -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
}