From 13239653127f983a625c241d718b65a13a9d350e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 01:40:08 -0800 Subject: [PATCH] Recolor Artwork: Tint shows only with art and previews the color the art takes A dialog opened on a color group or on loose colors no longer offers Tint for a spot color of the library: OK saves only the new colors, so a tint set there was lost. The row shows the process modes instead. The Tint rail and a tinted row's new color chip show a Lab spot color as art linked to it shows it: the working-CMYK equivalent while the Spot Colors options use CMYK values, with its inks scaled. --- crates/ui-egui/src/dialogs/recolor.rs | 55 +++++++++++++++++++++++++-- crates/ui-egui/src/tests/recolor.rs | 15 ++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/dialogs/recolor.rs b/crates/ui-egui/src/dialogs/recolor.rs index a5fce3543..4614b52b5 100644 --- a/crates/ui-egui/src/dialogs/recolor.rs +++ b/crates/ui-egui/src/dialogs/recolor.rs @@ -419,6 +419,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut let mut sel = selection(d); let mut clicked: Option = None; let mut pick: Option = None; + let shown: Vec = rows.iter().map(|r| new_shown(app, d, r)).collect(); widgets::list_box(ui, |ui| { egui::ScrollArea::vertical().id_salt("recolor-rows").max_height(TABLE_H).show(ui, |ui| { ui.set_width(ui.available_width()); @@ -451,7 +452,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut let (nr, _) = ui.allocate_exact_size(vec2(46.0, 20.0), Sense::hover()); let nresp = ui.interact(nr, egui::Id::unique((NEW_CHIP, i)), Sense::click()); let to = to_color(row).unwrap_or_default(); - chip(ui, nr, to.tinted(row_tint(row)), off); + chip(ui, nr, shown.get(i).copied().unwrap_or_default(), off); if nresp.clicked() { clicked = Some(i); } @@ -602,6 +603,25 @@ fn row_tint(row: &Value) -> f32 { row["tint"].as_f64().map_or(1.0, |t| (t / 100.0).clamp(0.0, 1.0) as f32) } +/// Tint `t` (0..1) of spot color `c` as art linked to it shows it: a Lab spot color is the active +/// document's working-CMYK equivalent while the Spot Colors options use CMYK values +/// ([`vectorcraft_doc::Document::linked_color`]), and its inks scale. +fn spot_tinted(app: &VectorcraftApp, c: Color, t: f32) -> Color { + app.session.active().map_or(c, |st| st.doc.linked_color(c, true)).tinted(t) +} + +/// The color row `row`'s new color chip shows: what the art takes, at the row's tint (a spot color +/// of the library as [`spot_tinted`] shows it). +fn new_shown(app: &VectorcraftApp, d: &Dialog, row: &Value) -> Color { + let to = to_color(row).unwrap_or_default(); + // Only a Lab spot color can show another color; the library lookup is skipped for the rest. + if matches!(to, Color::Lab { .. }) && d.bool("__art") && spot_of(app, d, &to).is_some() { + spot_tinted(app, to, row_tint(row)) + } else { + to.tinted(row_tint(row)) + } +} + /// The `sliderMode` id of `m` (`hsb`, `rgb`, `cmyk`, `lab`). fn mode_id(m: Mode) -> String { serde_json::to_value(m).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default() @@ -652,7 +672,8 @@ impl SliderTarget { fn of(app: &VectorcraftApp, d: &Dialog, rows: &[Value]) -> Self { let row = selection(d).into_iter().filter(|i| *i < rows.len()).min(); let color = row.and_then(|i| to_color(&rows[i])); - let spot = color.and_then(|c| spot_of(app, d, &c)); + // A row tint acts only on art, so without art a spot color shows the process modes. + let spot = color.filter(|_| d.bool("__art")).and_then(|c| spot_of(app, d, &c)); Self { row, color, spot } } @@ -733,7 +754,7 @@ fn sliders(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value] // Tint of the spot color, and its name. let tint = f64::from(row_tint(&rows[i])) * 100.0; let shown = widgets::SliderValue::field(tint, 0.0..=100.0, "%", 0); - if let Some(v) = slider_cell(ui, usize::MAX, "T", shown, "%", &|x| c32(&c.tinted(x))) { + if let Some(v) = slider_cell(ui, usize::MAX, "T", shown, "%", &|x| c32(&spot_tinted(app, c, x))) { rows[i]["tint"] = json!(v); changed = true; } @@ -1005,6 +1026,34 @@ mod tests { assert!(super::rows(&other)[0].get("tint").is_none(), "{:?}", super::rows(&other)); } + /// With the Spot Colors options on CMYK values, a row whose new color is a Lab spot color of the + /// library shows the art's color at its tint: the working-CMYK equivalent with its inks scaled. + #[test] + fn a_tinted_lab_spot_row_shows_the_cmyk_color_the_art_takes() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + let lab = json!({"l": 50, "a": 60, "b": -20}); + app.run("swatch.new", json!({"name": "Lab Ink", "color": lab, "spot": true})).unwrap(); + let data = app.run("swatch.library.save", json!({"names": ["Lab Ink"], "name": "Inks"})).unwrap()["data"].as_str().unwrap().to_string(); + let lib = app.run("swatch.library.load", json!({"data": data, "name": "inks.vcswatches"})).unwrap()["library"].as_str().unwrap().to_string(); + app.run("swatch.delete", json!({"names": ["Lab Ink"]})).unwrap(); + app.run("swatch.spotOptions", json!({"useLab": false})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone(); + app.run("paint.setFill", json!({"ids": [id], "color": lab})).unwrap(); + app.run("paint.setStroke", json!({"ids": [id], "none": true})).unwrap(); + app.run("select.all", json!({})).unwrap(); + app.run("ui.recolorDialog", json!({"library": lib})).unwrap(); + let mut d = app.ui.dialog.clone().unwrap(); + sync(&mut app, &mut d); + let mut rs = super::rows(&d); + rs[0]["tint"] = json!(50.0); + let c = to_color(&rs[0]).unwrap(); + assert!(matches!(c, Color::Lab { .. }), "{c:?}"); + let cmyk = app.session.doc().unwrap().doc.linked_color(c, true); + assert!(matches!(cmyk, Color::Cmyk { .. }), "{cmyk:?}"); + assert_eq!(new_shown(&app, &d, &rs[0]), cmyk.tinted(0.5)); + } + /// Runs one frame of the open dialog at `time` and returns row `i`'s new color chip, when the /// Recolor Artwork dialog drew it. fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, events: Vec, i: usize) -> Option { diff --git a/crates/ui-egui/src/tests/recolor.rs b/crates/ui-egui/src/tests/recolor.rs index 277f0bf8f..cdd8a5fdd 100644 --- a/crates/ui-egui/src/tests/recolor.rs +++ b/crates/ui-egui/src/tests/recolor.rs @@ -458,6 +458,21 @@ fn a_process_swatch_before_a_spot_one_of_its_color_shows_no_tint() { assert!(h.query_by_role_and_label(egui::accesskit::Role::Slider, "C").is_some(), "the CMYK sliders"); } +/// Opened on a color group without art, a row whose new color is a spot color of the library shows +/// the process modes and no Tint: OK saves only the group's colors, so a tint would be lost. +#[test] +fn a_spot_row_of_a_group_without_art_shows_no_tint() { + let mut app = app(); + let lib = library_of(&mut app, &[json!({"name": "Mix Ink", "color": {"c": 0, "m": 1, "y": 0, "k": 0}, "spot": true})]); + app.run("swatch.newGroup", json!({"name": "Mix", "colors": ["cmyk 0 100 0 0"], "keepModel": true})).unwrap(); + let mut h = assign_harness(app, json!({"group": "Mix", "library": lib})); + assert_ne!(field(h.state(), "__art"), json!(true), "no art"); + set(h.state_mut(), "__sel", json!([0])); + h.run_steps(2); + assert!(h.query_by_role_and_label(egui::accesskit::Role::Slider, "T").is_none(), "no Tint without art"); + assert!(h.query_by_role_and_label(egui::accesskit::Role::Slider, "C").is_some(), "the CMYK sliders"); +} + /// A spot color whose components are finer than a row's color key still shows Tint: the row's /// rounded color snaps back to it, as `recolor.apply` snaps it. #[test]