Merge pull request #4705 from dotCooCoo/fix/trace-library-global-swatch-same-color

Image Trace: Document Library links to a global swatch that shares a process swatch's color
This commit is contained in:
BFlat
2026-10-31 00:52:27 +07:00
committed by GitHub
3 changed files with 54 additions and 7 deletions
+14 -6
View File
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use vectorcraft_color::Swatch;
use vectorcraft_doc::ImageObject;
@@ -557,6 +559,16 @@ pub(super) fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Val
let unit = xf.determinant().abs().sqrt();
let (npaths, anchors, colors) = (res.paths.len(), res.anchor_count(), res.palette.len());
let label = if expand { "Image Trace (Make and Expand)" } else { "Image Trace" };
// A library color fills with the swatch's own color (its model kept), linked to a
// global or spot swatch of the document. Of the swatches that trace as this color,
// the first linked one is taken, else the first. Build the lookup once per trace.
let mut by_color: HashMap<[u8; 3], &Paint> = HashMap::new();
for (rgb, paint) in &library {
let chosen = by_color.entry(*rgb).or_insert(paint);
if !matches!(**chosen, Paint::Solid { swatch: Some(_), .. }) && matches!(paint, Paint::Solid { swatch: Some(_), .. }) {
*chosen = paint;
}
}
let id = s.edit(label, |d, sel| {
let src = d.node(target).cloned().ok_or(EngineError::NoNode(target))?;
let mut image = if is_trace_group(&src) {
@@ -584,12 +596,8 @@ pub(super) fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Val
*path = shape.to_path();
*live = Some(Box::new(shape));
}
// A library color fills with the swatch's own color (its model kept), linked to a
// global or spot swatch of the document.
let solid = library
.iter()
.find(|(rgb, _)| *rgb == tp.color)
.map_or_else(|| Paint::solid(Color::rgb8(tp.color[0], tp.color[1], tp.color[2])), |l| l.1.clone());
let solid =
by_color.get(&tp.color).map_or_else(|| Paint::solid(Color::rgb8(tp.color[0], tp.color[1], tp.color[2])), |paint| (*paint).clone());
// Gradients: a path the tracer found to be a linear gradient is filled with it.
let paint = tp.gradient.as_ref().map_or(solid, |g| traced_gradient(g, xf));
n.appearance = match tp.stroke {
+37
View File
@@ -3178,6 +3178,43 @@ fn image_trace_document_library_links_fills_to_global_swatches() {
assert!(!got.is_empty() && got.iter().all(|f| f.1.is_none()), "{got:?}");
}
#[test]
fn image_trace_document_library_links_to_a_global_swatch_after_a_process_swatch_of_its_color() {
let mut s = session();
// A process swatch, then two global swatches, all of one color.
s.execute("swatch.new", &json!({"name": "Red", "color": "#c82828"})).unwrap();
s.execute("swatch.new", &json!({"name": "Ink", "color": "#c82828", "global": true})).unwrap();
s.execute("swatch.new", &json!({"name": "Ink 2", "color": "#c82828", "global": true})).unwrap();
s.execute("swatch.new", &json!({"name": "Leaf", "color": "#28a03c"})).unwrap();
s.execute("swatch.new", &json!({"name": "Sky", "color": "#2846c8", "global": true})).unwrap();
let [tr, tg, tb, _] = s.doc().unwrap().doc.tint_paint("Sky", 0.5).unwrap().color().unwrap().to_rgba8(1.0);
let tint_hex = format!("#{tr:02x}{tg:02x}{tb:02x}");
s.execute("swatch.new", &json!({"name": "Mist", "color": tint_hex})).unwrap();
s.execute("swatch.new", &json!({"swatch": "Sky", "tint": 50})).unwrap();
let r = vectorcraft_trace::Raster::from_fn(60, 60, |x, _| match x {
0..20 => [200, 40, 40, 255],
20..40 => [40, 160, 60, 255],
_ => [tr, tg, tb, 255],
});
add_image(&mut s, &r, Affine::IDENTITY);
let params = json!({"mode": "color", "palette": "documentLibrary", "library": "document", "colors": 8});
let res = s.execute("imageTrace.makeAndExpand", &json!({"params": params})).unwrap();
let g = node(&s, NodeId(res["id"].as_u64().unwrap()));
let got: Vec<Paint> = g.children().unwrap().iter().map(|n| n.appearance.fill_paint()).collect();
let red = Color::rgb8(200, 40, 40);
let reds: Vec<&Paint> = got.iter().filter(|p| p.color().map(|c| c.to_rgba8(1.0)) == Some(red.to_rgba8(1.0))).collect();
assert!(!reds.is_empty(), "{got:?}");
assert!(reds.iter().all(|p| matches!(p, Paint::Solid { swatch: Some(w), .. } if w == "Ink")), "{got:?}");
let tints: Vec<&Paint> = got.iter().filter(|p| p.color().map(|c| c.to_rgba8(1.0)) == Some([tr, tg, tb, 255])).collect();
assert!(!tints.is_empty(), "{got:?}");
assert!(tints.iter().all(|p| matches!(p, Paint::Solid { swatch: Some(w), tint, .. } if w == "Sky" && *tint == 0.5)), "{got:?}");
// Editing the global swatch recolors the trace.
s.execute("swatch.edit", &json!({"name": "Ink", "color": "#ff8800"})).unwrap();
let g = node(&s, NodeId(res["id"].as_u64().unwrap()));
let orange = Color::rgb8(255, 136, 0).to_rgba8(1.0);
assert!(g.children().unwrap().iter().any(|n| n.appearance.fill_paint().color().map(|c| c.to_rgba8(1.0)) == Some(orange)));
}
#[test]
fn image_trace_document_library_matches_a_lab_spot_by_its_cmyk_equivalent() {
let _g = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
+3 -1
View File
@@ -49,7 +49,9 @@ and traces as All.
**Document Library linked colors.** With Library › Document Swatches, with or without a color group,
a traced fill or stroke whose color comes from a global or spot swatch is linked to that swatch, and
one whose color comes from a tint swatch is linked to its base at the tint. Editing the swatch
recolors the trace. Colors from process swatches and from swatch libraries are not linked.
recolors the trace. Colors from process swatches and from swatch libraries are not linked. When
several swatches show the same color, the first of them in the list that is a global, spot or tint
swatch gives the link.
**Document Library** traces with the library's colors as they are. Each pixel takes the library
color nearest to it in lightness and hue, with a difference in hue or chroma counted three times