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.
This commit is contained in:
dotCooCoo
2026-10-31 23:52:06 -07:00
parent 46cb088eae
commit c50fe0220d
2 changed files with 77 additions and 6 deletions
+10 -6
View File
@@ -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<String> {
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
+67
View File
@@ -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<Value> = 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() {