M3.79: Edit or Apply Color Group opens Recolor Artwork on a colour group

Double-clicking a colour group's folder in the Swatches panel, or the options
button (Edit or Apply Color Group) while a group is selected, opens Recolor
Artwork on the group: without art the rows are the group's colours and OK
rewrites the group; with art selected the art is reduced to the group's size,
takes its colours, and art and group change in one undo step. New
swatch.editGroup {group, colors, rename?} rewrites a group in order (art linked
to its global swatches follows); recolor.apply takes group, groupColors and
rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 04:20:27 +07:00
co-authored by Claude Opus 5.5
parent fca068a0a7
commit fcc0f4ca0f
9 changed files with 401 additions and 57 deletions
+25 -3
View File
@@ -30,8 +30,8 @@ pub fn specs() -> Vec<CommandSpec> {
"Recolor Artwork",
["Edit", "Edit Colors"],
None,
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null, exclude?: false (the row's colours are kept)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library (new colours snap to its nearest colour), includeImages?: true, includePatterns?: true} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces) as one undo step → {changed}",
has_selection,
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null, exclude?: false (the row's colours are kept)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library (new colours snap to its nearest colour), group?: colour group rewritten with groupColors?: [colour] (default: the rows' new colours, in order, excluded rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces) and the group, as one undo step → {changed}",
has_doc,
apply
),
cmd!(
@@ -305,10 +305,32 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
}
}
let ids = s.doc()?.selection.objects.clone();
let group = str_param(p, "group");
if ids.is_empty() && group.is_none() {
return Err(bad(C, "select artwork to recolour, or give a colour `group`"));
}
let map = ColorMap::new(&rows, method);
let f = |c: Color| map.map(c);
let scope = Scope { fill: true, stroke: true, ..Scope::of(p) };
let changed = s.edit("Recolor Artwork", |d, _| Ok(Recolor::new(scope, &f).run(d, &ids)))?;
let label = if ids.is_empty() { "Edit Color Group" } else { "Recolor Artwork" };
let new: Vec<Color> = match p.get("groupColors").and_then(Value::as_array) {
Some(cs) => cs
.iter()
.map(|v| {
color_value(v).map(|c| palette.as_ref().map_or(c, |pl| pl.nearest(c))).ok_or_else(|| bad(C, format!("bad colour {v} in groupColors")))
})
.collect::<Result<_>>()?,
None => rows.iter().filter_map(Row::new_color).collect(),
};
let rename = str_param(p, "rename");
let (changed, edit) = s.edit(label, |d, _| {
let changed = Recolor::new(scope, &f).run(d, &ids);
let edit = group.map(|g| super::swatch::rewrite_group(d, g, &new, rename, C)).transpose()?;
Ok((changed, edit))
})?;
if let Some(e) = edit {
e.defaults(s);
}
Ok(json!({ "changed": changed }))
}
+84
View File
@@ -122,6 +122,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
swatch_spot_options
),
cmd!(
"swatch.editGroup",
"Edit Color Group",
["Window", "Swatches"],
None,
"{group: colour group, colors: [colour] (hex, key, {c,m,y,k} or {l,a,b}; the group's swatches take them in order, extra colours become new swatches, swatches past the end are removed; built-in swatches can't change), rename?: new group name} rewrite a colour group as one undo step; art linked to its global swatches takes their new colours (and keeps its colour, unlinked, when its swatch goes) → {name, swatches: [names], relinked}",
has_doc,
swatch_edit_group
),
]
}
@@ -770,3 +779,78 @@ fn follow_bases(bases: &[(String, Color)]) -> impl FnMut(&mut Color, &mut Option
std::mem::replace(c, new) != new
}
}
/// A colour group rewrite: its name afterwards and the swatch links it changed.
pub(super) struct GroupEdit {
name: String,
swatches: Vec<String>,
relinks: Vec<Relink>,
relinked: usize,
}
impl GroupEdit {
/// Apply the link changes to the default fill and stroke for new art.
pub(super) fn defaults(&self, s: &mut Session) {
self.relinks.iter().for_each(|r| r.defaults(s));
}
}
/// Rewrite colour group `group` with `colors`: its swatches take them in order (keeping their
/// names, kinds and global or spot flags), extra colours are added as new swatches and swatches
/// past the end are removed. Paints linked to its global swatches follow (a removed swatch's are
/// unlinked); `rename` renames the group. A built-in swatch (None, Registration) it would change or
/// remove is refused.
pub(super) fn rewrite_group(d: &mut Document, group: &str, colors: &[Color], rename: Option<&str>, cmd: &str) -> Result<GroupEdit> {
let gi = d.swatch_groups.iter().position(|g| g.name == group).ok_or_else(|| bad(cmd, format!("no colour group `{group}`")))?;
let name = match rename.map(str::trim).filter(|n| !n.is_empty() && *n != group) {
Some(n) => d.free_swatch_name(n),
None => group.to_string(),
};
let old = &d.swatch_groups[gi].swatches;
if let Some(w) = old.iter().enumerate().find(|(i, w)| w.is_reserved() && colors.get(*i).is_none_or(|c| w.paint.color() != Some(*c))).map(|x| x.1)
{
return Err(bad(cmd, format!("`{}` can't be edited or removed", w.name)));
}
let mut relinks = vec![];
let mut swatches = std::mem::take(&mut d.swatch_groups[gi].swatches);
for gone in swatches.split_off(colors.len().min(swatches.len())) {
if gone.global {
relinks.push(Relink { from: gone.name.clone(), to: gone.name, color: None, keep: false });
}
}
for (w, c) in swatches.iter_mut().zip(colors) {
if w.paint.color() != Some(*c) {
// A tint swatch becomes a colour of its own.
w.paint = Paint::solid(*c);
if w.global {
// Art linked to a Lab spot shows it as the Spot Colors options say.
relinks.push(Relink { from: w.name.clone(), to: w.name.clone(), color: Some(d.linked_color(*c, w.spot)), keep: true });
}
}
}
for c in colors.iter().skip(swatches.len()) {
let nm = unique_name(&color_name(*c), |n| n == name || d.swatch_name_taken(n) || swatches.iter().any(|w| w.name == n));
swatches.push(Swatch { name: nm, paint: Paint::solid(*c), global: false, spot: false });
}
let g = &mut d.swatch_groups[gi];
(g.name, g.swatches) = (name.clone(), swatches);
let names = g.swatches.iter().map(|w| w.name.clone()).collect();
let relinked = if relinks.is_empty() { 0 } else { d.map_solid_paints(&mut |c, l, t| relinks.iter().any(|r| r.apply(c, l, t))) };
Ok(GroupEdit { name, swatches: names, relinks, relinked })
}
fn swatch_edit_group(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.editGroup";
let group = str_param(p, "group").ok_or_else(|| bad(C, "missing `group`"))?;
let colors: Vec<Color> = p
.get("colors")
.and_then(Value::as_array)
.ok_or_else(|| bad(C, "missing `colors`"))?
.iter()
.map(|v| color_value(v).ok_or_else(|| bad(C, format!("bad colour {v}"))))
.collect::<Result<_>>()?;
let rename = str_param(p, "rename");
let e = s.edit("Edit Color Group", |d, _| rewrite_group(d, group, &colors, rename, C))?;
e.defaults(s);
Ok(json!({"name": e.name, "swatches": e.swatches, "relinked": e.relinked}))
}