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() {