Merge pull request #4777 from dotCooCoo/fix/trace-library-unlinked-fills

Image Trace: Document Library fills are not linked to swatches
This commit is contained in:
BFlat
2026-10-31 07:33:13 +07:00
committed by GitHub
4 changed files with 56 additions and 39 deletions
+8 -16
View File
@@ -222,18 +222,16 @@ fn trace_params(s: &Session, c: &str, p: &Value) -> Result<tr::TraceParams> {
Ok(params)
}
/// Document swatch `w`'s solid color with the paint a trace fills with it: a global or spot color,
/// or a tint of one, linked to its swatch as `swatch_solid` (paint.rs) links it; any other color
/// unlinked. The color is the one the paint shows (a Lab spot color's CMYK equivalent when the Spot
/// Colors options use CMYK values).
/// Document swatch `w`'s displayed solid color and unlinked trace paint. A tint uses its tinted
/// color; a Lab spot color uses its CMYK equivalent when the Spot Colors options use CMYK values.
fn trace_swatch_paint(d: &Document, w: &Swatch) -> Option<(Color, Paint)> {
let linked = d.swatch_link(&w.name).and_then(|(base, tint)| d.tint_paint(&base, tint));
let color = linked.as_ref().and_then(Paint::color).or_else(|| w.paint.color())?;
Some((color, linked.unwrap_or_else(|| Paint::solid(color))))
Some((color, Paint::solid(color)))
}
/// Document Library with `colorGroup`: the solid colors of the active document's color group
/// `name` (so named exactly, else ignoring case), each with its paint ([`trace_swatch_paint`]).
/// `name` (so named exactly, else ignoring case), each with its unlinked paint ([`trace_swatch_paint`]).
/// Errors name command `c`. ([`trace_params`] has checked that the library is the document's
/// swatches.)
fn trace_group_colors(c: &str, s: &Session, name: &str) -> Result<Vec<(Color, Paint)>> {
@@ -249,9 +247,7 @@ fn trace_group_colors(c: &str, s: &Session, name: &str) -> Result<Vec<(Color, Pa
}
/// Document Library in Color mode: look the library's colors (or its color group's) up into
/// `params.swatches` → each one as traced (sRGB) with the paint to fill with. The document's
/// global and spot colors fill linked to their swatches ([`trace_swatch_paint`]); a swatch
/// library's colors fill unlinked. Errors name command `c`.
/// `params.swatches` → each one as traced (sRGB) with its unlinked paint. Errors name command `c`.
fn trace_library(c: &str, s: &Session, params: &mut tr::TraceParams) -> Result<Vec<([u8; 3], Paint)>> {
if params.mode != tr::Mode::Color || params.palette != tr::Palette::DocumentLibrary {
return Ok(vec![]);
@@ -559,15 +555,11 @@ 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.
// A library color fills with the swatch's displayed color (its model kept), unlinked.
// Keep the first swatch that traces as a given color. 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;
}
by_color.entry(*rgb).or_insert(paint);
}
let id = s.edit(label, |d, sel| {
let src = d.node(target).cloned().ok_or(EngineError::NoNode(target))?;
+43 -16
View File
@@ -3188,7 +3188,7 @@ fn image_trace_document_library_uses_a_color_group() {
}
#[test]
fn image_trace_document_library_links_fills_to_global_swatches() {
fn image_trace_document_library_fills_are_unlinked() {
let mut s = session();
// A global color, a tint of another global color and a process color.
s.execute("swatch.new", &json!({"name": "Ink", "color": "#c82828", "global": true})).unwrap();
@@ -3208,7 +3208,7 @@ fn image_trace_document_library_links_fills_to_global_swatches() {
});
let img = add_image(&mut s, &r, Affine::IDENTITY);
let params = json!({"mode": "color", "palette": "documentLibrary", "library": "document", "colors": 3});
// Each fill as (its color, the swatch it links to, its tint), one per color.
// Each fill as (its color, its swatch link, its tint), one per color.
let fills = |s: &Session, id: &Value| {
let mut v: Vec<(String, Option<String>, f32)> = node(s, NodeId(id.as_u64().unwrap()))
.children()
@@ -3230,27 +3230,26 @@ fn image_trace_document_library_links_fills_to_global_swatches() {
let res = s.execute("imageTrace.makeAndExpand", &json!({"params": params})).unwrap();
let got = fills(&s, &res["id"]);
assert_eq!(got.len(), 3, "{got:?}");
assert!(got.contains(&fill("#c82828", Some("Ink"), 1.0)), "{got:?}");
assert!(got.contains(&fill(&tint, Some("Sky"), 0.5)), "{got:?}");
assert!(got.contains(&fill("#c82828", None, 1.0)), "{got:?}");
assert!(got.contains(&fill(&tint, None, 1.0)), "{got:?}");
assert!(got.contains(&fill("#28a03c", None, 1.0)), "{got:?}");
// Editing the global swatches recolors the trace: the tint at its own tint.
// Editing the global swatches leaves the traced colors unchanged.
s.execute("swatch.edit", &json!({"name": "Ink", "color": "#ff8800"})).unwrap();
s.execute("swatch.edit", &json!({"name": "Sky", "color": "#000000"})).unwrap();
let tint = hex_of(&mut s, "Sky 50%");
let got = fills(&s, &res["id"]);
assert!(got.contains(&fill("#ff8800", Some("Ink"), 1.0)), "{got:?}");
assert!(got.contains(&fill(&tint, Some("Sky"), 0.5)), "{got:?}");
assert!(got.contains(&fill("#c82828", None, 1.0)), "{got:?}");
assert!(got.contains(&fill(&tint, None, 1.0)), "{got:?}");
assert!(got.contains(&fill("#28a03c", None, 1.0)), "{got:?}");
for _ in 0..3 {
s.execute("edit.undo", &json!({})).unwrap();
}
// A color group's global swatches link too.
// A color group's global swatches also fill without links.
s.execute("swatch.newGroup", &json!({"name": "Brand", "swatches": ["Ink", "Leaf"]})).unwrap();
let mut group = params.clone();
group["colorGroup"] = json!("Brand");
let res = s.execute("imageTrace.makeAndExpand", &json!({"id": img.0, "params": group})).unwrap();
let got = fills(&s, &res["id"]);
assert!(got.contains(&fill("#c82828", Some("Ink"), 1.0)) && got.contains(&fill("#28a03c", None, 1.0)), "{got:?}");
assert!(got.contains(&fill("#c82828", None, 1.0)) && got.contains(&fill("#28a03c", None, 1.0)), "{got:?}");
s.execute("edit.undo", &json!({})).unwrap();
// A swatch library's colors stay unlinked.
let mut web = params.clone();
@@ -3261,7 +3260,7 @@ fn image_trace_document_library_links_fills_to_global_swatches() {
}
#[test]
fn image_trace_document_library_links_to_a_global_swatch_after_a_process_swatch_of_its_color() {
fn image_trace_document_library_same_color_stays_unlinked_in_swatch_order() {
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();
@@ -3286,15 +3285,43 @@ fn image_trace_document_library_links_to_a_global_swatch_after_a_process_swatch_
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:?}");
assert!(reds.iter().all(|p| matches!(p, Paint::Solid { swatch: None, .. })), "{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.
assert!(tints.iter().all(|p| matches!(p, Paint::Solid { swatch: None, .. })), "{got:?}");
// Editing the global swatch leaves the trace unchanged.
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)));
assert!(!g.children().unwrap().iter().any(|n| n.appearance.fill_paint().color().map(|c| c.to_rgba8(1.0)) == Some(orange)));
assert_eq!(g.children().unwrap().iter().map(|n| n.appearance.fill_paint()).collect::<Vec<_>>(), got);
}
#[test]
fn image_trace_document_library_same_color_is_unlinked_in_both_swatch_orders() {
let trace_fill = |global_first: bool| {
let mut s = session();
for global in [global_first, !global_first] {
s.execute("swatch.new", &json!({"name": if global { "Ink" } else { "Red" }, "color": "#c82828", "global": global})).unwrap();
}
let r = vectorcraft_trace::Raster::from_fn(60, 60, |_, _| [200, 40, 40, 255]);
add_image(&mut s, &r, Affine::IDENTITY);
let res = s
.execute(
"imageTrace.makeAndExpand",
&json!({"params": {"mode": "color", "palette": "documentLibrary", "library": "document", "colors": 2}}),
)
.unwrap();
let g = node(&s, NodeId(res["id"].as_u64().unwrap()));
let fills: Vec<Paint> = g.children().unwrap().iter().map(|n| n.appearance.fill_paint()).collect();
assert!(!fills.is_empty());
assert!(fills.iter().all(|p| matches!(p, Paint::Solid { swatch: None, .. })), "{fills:?}");
fills
};
let process_first = trace_fill(false);
let global_first = trace_fill(true);
assert_eq!(process_first, global_first);
assert!(process_first.iter().all(|p| p.color() == Some(Color::rgb8(200, 40, 40))));
}
#[test]
@@ -3337,7 +3364,7 @@ fn image_trace_document_library_matches_a_lab_spot_by_its_cmyk_equivalent() {
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 spot = Paint::Solid { color: shown, swatch: Some("Lab Ink".into()), tint: 1.0 };
let spot = Paint::solid(shown);
assert!(got.contains(&spot), "{got:?}");
assert!(!got.iter().any(|p| p.color() == Some(process)), "{got:?}");
}
+4 -6
View File
@@ -46,12 +46,10 @@ most `colors` of its colors are used. A missing group and a group with no solid
mode, palette or library in the panel clears the group. A group the active document does not have shows as All
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. 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 colors.** With Library › Document Swatches, with or without a color group,
traced fills and strokes take the library color without a link to its swatch. This includes global,
spot and tint swatches. Tints trace at the color shown by the tint swatch. Editing a swatch later
does not recolor the trace. Colors from other swatch libraries are also unlinked.
**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
+1 -1
View File
@@ -865,7 +865,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Disable, Enable, Unlink and Link Opacity Mask and Opacity Mask Options (`transparency.setOpacityMask`) record no undo step when every mask they act on already has the flags they set.
- **Image Trace, thin closed outlines:** generated `fit::snap_area_tests` verify nonzero interiors after straightening, opposite closed-boundary winding and Noise controls; see [quality notes](image-trace-quality.md#thin-closed-outlines-with-snap-curves-to-lines). Scores and hours unchanged; open-piece fitting is unchanged.
- **Image Trace, Document Library color groups:** with Document Swatches, the panel's Colors menu and `"colorGroup"` trace with one color group's colors instead of every swatch, as the public panel documentation describes Document Library; code-drawn tests in `crates/engine` and the panel.
- **Image Trace, Document Library linked colors:** with Document Swatches, traced fills and strokes from global and spot swatches (and tints of them) are linked to their swatches, so editing a swatch recolors the trace; process colors and swatch-library colors stay unlinked; a code-drawn test in `crates/engine`.
- **Image Trace, Document Library colors:** with Document Swatches, traced fills and strokes use the displayed colors of process, global, spot and tint swatches without links, so editing a swatch leaves the trace unchanged; swatch-library colors are also unlinked; code-drawn tests in `crates/engine`.
- **Image Trace, flattened slivers:** in a Color, Grayscale or Black and White trace with Fills, a traced region whose outline straightens onto one line (a single pixel, or a one-pixel row along an edge, its pieces shared with the regions beside it) is left out instead of becoming a selectable path with no area, which paints nothing. Snap Curves To Lines straightens more outlines, so it leaves out more of these. Without Fills, the region's 1 px stroke draws it, and it is kept. Code-drawn tests in `crates/trace`.
- **Image Trace, Auto Color rare colours:** the Automatic palette keeps colours too rare for its area count when they are distinct (Oklab) and drawn as coherent regions, at most six, protected from mixed-colour merging and Noise; code-drawn stem, highlight, low-contrast and scattered-noise tests (see [quality notes](image-trace-quality.md#auto-color-keeps-rare-colours)).
- **Image Trace, Document Library keeps neutral tones neutral:** each pixel takes the library color nearest in Oklab with differences in hue and chroma counted three times, so a gray or a muted brown traces as a neutral library color instead of a saturated one of similar lightness; a code-drawn test of gray and brown patches with a black, light and blue library in `crates/trace`.