From c50fe0220d0a59573faa8b517a446ce49135d269 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 23:52:06 -0700 Subject: [PATCH] Recolor Artwork: Reset discards slider edits and reads the art's own colors Reset now also clears the slider edit, so the preview and OK keep each color's model again instead of sending model "new". Changing the Colors count, a Preserve flag or Limit to Library, or pressing Reset, with the preview on reduced the previewed colors, so the rows no longer matched the art; the preview is now rolled back before the colors are reduced. The Tint mode follows the swatch recolor.apply links: the first library swatch of the library color nearest the row's new color, shown as Tint only when that swatch is a spot color. --- crates/ui-egui/src/dialogs/recolor.rs | 16 ++++--- crates/ui-egui/src/tests/recolor.rs | 67 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/dialogs/recolor.rs b/crates/ui-egui/src/dialogs/recolor.rs index 30d22a4ba..7041f96ca 100644 --- a/crates/ui-egui/src/dialogs/recolor.rs +++ b/crates/ui-egui/src/dialogs/recolor.rs @@ -163,6 +163,10 @@ fn sync(app: &mut VectorcraftApp, d: &mut Dialog) { if !d.bool("__art") || d.fields.get(DONE) == Some(&sig) { return; } + // The art's own colors are reduced, not those of the preview, which runs again afterwards. + if d.fields.remove(super::form::PREVIEWED).is_some() { + let _ = app.session.cancel_interaction(); + } let mut p = sig.clone(); p["method"] = json!(d.str("method")); match app.session.execute("recolor.reduce", &p) { @@ -488,6 +492,8 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut } if widgets::icon_button(ui, "rotate-ccw", tl!("Reset the rows to the artwork's colors"), false, 24.0).clicked() { d.fields.remove("__reduced"); + d.fields.remove(SLID); + d.fields.remove(COMPS); } let target = SliderTarget::of(app, d, rows); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| mode_menu(ui, d, &target)); @@ -572,17 +578,15 @@ 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() } -/// The name of the spot color of the library (Limit to Library) that new color `c` is. +/// The name of the library swatch (Limit to Library) that `recolor.apply` gives new color `c`, when +/// it is a spot color: the first swatch of the library color nearest `c`. fn spot_of(app: &VectorcraftApp, d: &Dialog, c: &Color) -> Option { let lib = d.str("limitTo"); if lib.is_empty() { return None; } - let k = ColorKey::of(c); - swatchlib::library_swatches(&app.session, &lib)? - .into_iter() - .find(|w| w.spot && w.paint.color().is_some_and(|x| ColorKey::of(&x) == k)) - .map(|w| w.name) + let near = swatchlib::limit_palette(&app.session, &lib)?.nearest(*c); + swatchlib::library_swatches(&app.session, &lib)?.into_iter().find(|w| w.paint.color() == Some(near)).filter(|w| w.spot).map(|w| w.name) } /// The mode the sliders show new color `c` in, `None` for Tint: the mode picked in the menu, else diff --git a/crates/ui-egui/src/tests/recolor.rs b/crates/ui-egui/src/tests/recolor.rs index af6991d53..c4aa85d6e 100644 --- a/crates/ui-egui/src/tests/recolor.rs +++ b/crates/ui-egui/src/tests/recolor.rs @@ -346,6 +346,73 @@ fn the_tint_slider_sets_a_spot_rows_tint() { } } +/// Makes the swatches of the `swatch.new` params `swatches`, saves them as a library and loads it, +/// removes them from the document again, and returns the library's id. +fn library_of(app: &mut VectorcraftApp, swatches: &[Value]) -> String { + let names: Vec = swatches.iter().map(|s| s["name"].clone()).collect(); + for s in swatches { + app.run("swatch.new", s.clone()).unwrap(); + } + let data = app.run("swatch.library.save", json!({"names": names, "name": "Lib"})).unwrap()["data"].as_str().unwrap().to_string(); + let lib = app.run("swatch.library.load", json!({"data": data, "name": "lib.vcswatches"})).unwrap()["library"].as_str().unwrap().to_string(); + app.run("swatch.delete", json!({"names": names})).unwrap(); + lib +} + +/// When the library's first swatch of a row's new color is a process color, the row shows no Tint, +/// even with a spot color of that color after it: `recolor.apply` gives the art the first one. +#[test] +fn a_process_swatch_before_a_spot_one_of_its_color_shows_no_tint() { + let mut app = app(); + let lib = library_of( + &mut app, + &[ + json!({"name": "Mix Process", "color": {"c": 0, "m": 1, "y": 0, "k": 0}}), + json!({"name": "Mix Ink", "color": {"c": 0, "m": 1, "y": 0, "k": 0}, "spot": true}), + ], + ); + art(&mut app, &["cmyk 0 100 0 0"]); + let mut h = assign_harness(app, json!({"library": lib})); + 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 for a process swatch"); + 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] +fn a_spot_color_finer_than_the_rows_key_shows_tint() { + let mut app = app(); + let lib = library_of(&mut app, &[json!({"name": "Mix Fine", "color": {"c": 0, "m": 0.12345, "y": 0, "k": 0}, "spot": true})]); + art(&mut app, &["cmyk 0 12 0 0"]); + let mut h = assign_harness(app, json!({"library": lib})); + set(h.state_mut(), "__sel", json!([0])); + h.run_steps(2); + assert!(h.query_by_role_and_label(egui::accesskit::Role::Slider, "T").is_some(), "the spot row shows Tint"); +} + +/// Reset with the preview on reads the art's own colors again and discards a slider edit: OK then +/// gives RGB art a CMYK color group's color in RGB, as it does without the edit. +#[test] +fn reset_after_a_slider_edit_keeps_the_arts_model() { + let mut app = app(); + app.run("swatch.newGroup", json!({"name": "Mix", "colors": ["cmyk 0 100 0 0"], "keepModel": true})).unwrap(); + art(&mut app, &["#ff00ff"]); + let mut h = assign_harness(app, json!({"group": "Mix"})); + set(h.state_mut(), "__sel", json!([0])); + h.run_steps(2); + click_slider(&mut h, "C", 0.5); + assert_eq!(field(h.state(), "__slid"), json!(true), "the sliders edited the row"); + h.get_by_role_and_label(egui::accesskit::Role::Button, "Reset the rows to the artwork's colors").click(); + h.run_steps(3); + assert_eq!(field(h.state(), "rows")[0]["from"], json!(["rgb 255 0 255"]), "Reset reads the art's colors, not the preview's"); + h.get_by_label("OK").click(); + h.run_steps(2); + assert!(h.state().ui.dialog.is_none()); + assert!(matches!(first_fill(h.state()).color(), Some(Color::Rgb { .. })), "the art stays RGB: {:?}", first_fill(h.state())); +} + /// The dialog keeps its height when the tab or the sliders' color mode changes. #[test] fn switching_tabs_or_slider_modes_keeps_the_dialog_height() {