diff --git a/crates/engine/src/cmd/buildcmds/trace.rs b/crates/engine/src/cmd/buildcmds/trace.rs index 832219d5f..00bcc1c83 100644 --- a/crates/engine/src/cmd/buildcmds/trace.rs +++ b/crates/engine/src/cmd/buildcmds/trace.rs @@ -222,18 +222,16 @@ fn trace_params(s: &Session, c: &str, p: &Value) -> Result { 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> { @@ -249,9 +247,7 @@ fn trace_group_colors(c: &str, s: &Session, name: &str) -> Result Result> { 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 = 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))?; diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index 407064461..bd9bd02a2 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -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, 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::>(), 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 = 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 = 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:?}"); } diff --git a/docs/image-trace-adjustments.md b/docs/image-trace-adjustments.md index 0de13b953..92e5f42ac 100644 --- a/docs/image-trace-adjustments.md +++ b/docs/image-trace-adjustments.md @@ -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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d249198ce..edcba39a9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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`.