diff --git a/crates/engine/src/cmd/swatch.rs b/crates/engine/src/cmd/swatch.rs index d67d6561e..59bfa528f 100644 --- a/crates/engine/src/cmd/swatch.rs +++ b/crates/engine/src/cmd/swatch.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use serde_json::{Value, json}; -use vectorcraft_color::{Color, Gradient, Paint, Swatch, SwatchGroup, tint_percent}; +use vectorcraft_color::{Color, Gradient, GradientKind, GradientMethod, GradientStop, Paint, Swatch, SwatchGroup, tint_percent}; use vectorcraft_doc::pattern::pattern_paint; use vectorcraft_doc::swatches::{LinkFn, color_name, map_node_solid_paints, node_colors}; use vectorcraft_doc::{Document, NodeId}; @@ -320,10 +320,35 @@ fn float_key(v: f32) -> Option { } } -/// The keys of a gradient's stop colors, equal for equal gradients; `None` when a stop's color -/// equals no color, so the gradient equals no gradient. -fn gradient_key(g: &Gradient) -> Option> { - g.stops.iter().map(|s| color_key(&s.color)).collect() +/// A gradient stop's offset, color, opacity, midpoint, swatch link, tint and process plates, as +/// keys. +type StopKey<'a> = (u32, ColorKey, u32, u32, Option<&'a str>, u32, Option<[u32; 4]>); + +/// A gradient's kind, method, dither and stops, as keys. +type GradientKey<'a> = (u8, u8, bool, Vec>); + +/// A key that is equal for two gradients exactly when the gradients are equal (`==`); `None` when +/// any of its numbers is NaN, so the gradient equals no gradient. +fn gradient_key(g: &Gradient) -> Option> { + let kind = match g.kind { + GradientKind::Linear => 0, + GradientKind::Radial => 1, + GradientKind::Freeform => 2, + }; + let method = match g.method { + GradientMethod::Classic => 0, + GradientMethod::Perceptual => 1, + }; + Some((kind, method, g.dither, g.stops.iter().map(stop_key).collect::>()?)) +} + +/// A stop's key for [`gradient_key`]; `None` when any of its numbers is NaN. +fn stop_key(s: &GradientStop) -> Option> { + let plates = match s.plates { + Some(p) => Some([float_key(p[0])?, float_key(p[1])?, float_key(p[2])?, float_key(p[3])?]), + None => None, + }; + Some((float_key(s.offset)?, color_key(&s.color)?, float_key(s.opacity)?, float_key(s.midpoint)?, s.swatch.as_deref(), float_key(s.tint)?, plates)) } /// Link the solid paints in the subtrees of `ids` whose color is one of `links`' to that swatch @@ -891,16 +916,7 @@ fn swatch_unused(s: &mut Session, _: &Value) -> Result { let link_tints: HashSet<(&str, u32)> = links.iter().filter_map(|(l, t)| Some((l.as_str(), float_key(*t)?))).collect(); let colors: HashSet = colors.iter().filter_map(color_key).collect(); let patterns: HashSet<&String> = patterns.iter().collect(); - // Gradients by their stops' colors, each listed once. - let mut by_stops: HashMap, Vec<&Gradient>> = HashMap::new(); - for g in &gradients { - if let Some(k) = gradient_key(g) { - let list = by_stops.entry(k).or_default(); - if !list.contains(&g) { - list.push(g); - } - } - } + let gradients: HashSet = gradients.iter().filter_map(gradient_key).collect(); let used = |w: &Swatch| { gradient_links.contains(&w.name) || match w.tint_of() { @@ -910,7 +926,7 @@ fn swatch_unused(s: &mut Session, _: &Value) -> Result { || match &w.paint { Paint::None => true, Paint::Solid { color, .. } => color_key(color).is_some_and(|k| colors.contains(&k)), - Paint::Gradient(g) => gradient_key(&g.gradient).and_then(|k| by_stops.get(&k)).is_some_and(|list| list.contains(&&g.gradient)), + Paint::Gradient(g) => gradient_key(&g.gradient).is_some_and(|k| gradients.contains(&k)), Paint::Pattern { pattern, .. } => patterns.contains(pattern), } }; diff --git a/crates/engine/src/tests/swatchcmds.rs b/crates/engine/src/tests/swatchcmds.rs index 38a4ffcaa..0e7e60da3 100644 --- a/crates/engine/src/tests/swatchcmds.rs +++ b/crates/engine/src/tests/swatchcmds.rs @@ -207,15 +207,24 @@ fn unused_matches_links_tints_colors_gradients_and_patterns_exactly() { set_fill(&mut s, tinted, Paint::Solid { color: teal.tinted(0.4), swatch: Some("Teal".into()), tint: 0.4 }); let zero = rect_filled(&mut s, json!({"color": "#000000"})); set_fill(&mut s, zero, Paint::solid(Color::rgb(-0.0, 128.0 / 255.0, 1.0))); - // Sunset's gradient without a link to it, and Ocean's with one stop moved. + // Sunset's gradient without a link to it, beside gradients with Sunset's stop colors and a + // different midpoint or opacity; Ocean's with one stop moved, and Ocean's with another kind, + // method or dither. let gradient = |s: &Session, n: &str| match &doc(s).swatch(n).unwrap().paint { Paint::Gradient(g) => g.gradient.clone(), p => panic!("{p:?}"), }; let sunset = gradient(&s, "Sunset"); - let mut ocean = gradient(&s, "Ocean"); - ocean.stops[0].offset += 0.25; - for g in [sunset.clone(), sunset, ocean] { + let (mut midpoint, mut opacity) = (sunset.clone(), sunset.clone()); + midpoint.stops[0].midpoint = 0.3; + opacity.stops[1].opacity = 0.5; + let ocean = gradient(&s, "Ocean"); + let (mut moved, mut radial, mut perceptual, mut dither) = (ocean.clone(), ocean.clone(), ocean.clone(), ocean); + moved.stops[0].offset += 0.25; + radial.kind = vectorcraft_color::GradientKind::Radial; + perceptual.method = vectorcraft_color::GradientMethod::Perceptual; + dither.dither = true; + for g in [midpoint, sunset.clone(), opacity, sunset, moved, radial, perceptual, dither] { let id = rect_filled(&mut s, json!({"color": "#000000"})); set_fill(&mut s, id, Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(g)))); }