From da5f6e0db9f3785cc9fe75145bb61bd3804962ae Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 13:00:10 -0800 Subject: [PATCH] Swatches: Select All Unused looks gradients up by their whole definition swatch.unused grouped the gradients in use by their stop colors and compared each gradient swatch with every gradient in its group, so gradients that share stop colors but differ in offsets, midpoints, opacity, kind, method, dither or links still took quadratic time. Each gradient now has a key that is equal exactly when the gradients are equal: kind, method, dither, and each stop's offset, color, opacity, midpoint, link, tint and process plates. The gradients in use go into one hash set, and each gradient swatch is looked up in it. --- crates/engine/src/cmd/swatch.rs | 48 ++++++++++++++++++--------- crates/engine/src/tests/swatchcmds.rs | 17 +++++++--- 2 files changed, 45 insertions(+), 20 deletions(-) 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)))); }